Introduction
On 24 September 2026, the Dutch Institute for Vulnerability Disclosure (DIVD) announced publicly that they had suffered a security incident and had attributed it to an “agentic AI powered attack” given the forensic artifacts they discovered during the incident response. They attributed the source of the initial breach to Zammad, a helpdesk and ticketing application.
During their investigation, they determined that two distinct 0-day vulnerabilities were abused:
- CVE-2026-102489 – A session hijack that ultimately allows executing code as the zammad user
- CVE-2026-102490 – A privilege escalation that allows escalating to root
Soon after, DIVD released further details of the Zammad vulnerabilities and released a Indicators of Compromise script which looked for leaked session cookies in error logs.
CVE-2026-102489: Zammad Session Leak
Given the above details, it was classified as a session hijack and that session cookies would appear in the logs, so we dug in.
We prompted a vulnerability research harness, with Anthropic’s Opus 4.8 as the model, to attempt to identify and reproduce the issues given:
- The DIVD CSIRT cases
- The DIVD IOC script
- The Zammad open-source GitHub repository
After a couple hours, the results were back and the harness did confirm that if there was a way to trigger an error it would leak the session cookie strings, but it was unable to find an unauthenticated trigger request.
Fuzzing The Code
Re-prompting the agent, we advised it to build fuzzing tooling to target the areas of code it suspected most likely to be associated with the vulnerable area of code. Several minutes later into a fuzzing campaign against the websocket code, it triggered an error that was reflected back to the requestor which contained the session cookies we were looking for.
The vulnerability trigger is a simple single request to the WebSocket endpoint, /ws, with a payload of {“event”: “base”}.

The server then reflects this error back to the client, which leaks the cookies for all active users.

Understanding the Source
Starting from the architectural perspective, Zammad is a Ruby application that utilizes websockets. It stores all global connection states in the @clients class-level instance variable. The headers variable notably includes the session Cookie header.

Every event that occurs in the framework, the @clients variable is passed with it. There is no event type or client based filtering.

The base event handler for the session object takes every parameter passed to it and unpacks each key-value pair stored in it. Which means the sensitive cookies are stored inside the headers key of the clients variable.

Lastly, in sessions/event.rb, the run() method returns the entire event’s instance variables in a response to the client when an error occurs.

When a client sends {“event”:”base”}, the dispatcher successfully resolves Sessions::Event::Base as a real Ruby constant, instantiates it with the full @clients registry mass-assigned onto the object, then fails when .run is called – as Base has no implementation. Ruby constructs the NoMethodError message to include the receiver’s full object representation, which contains every instance variable including @clients, and sessions/event.rb returns that message string directly to the caller.
Authenticated Session to Code Execution
Once an attacker has a valid admin session cookie from the leak, they can write arbitrary files into the Zammad application directory via the package installation endpoint. By writing a malicious ERB template that overrides the built-in password reset email view, they can trigger its execution by initiating a password reset for any user – causing Zammad’s mailer to render the attacker-controlled template with full Ruby execution privileges, achieving remote code execution as the zammad OS user.

Our proof of concept exploit can be found here: https://github.com/horizon3ai/CVE-2026-102489
Given that the privilege escalation vector we believe to be CVE-2026-102490 remains unpatched, we will be withholding those details.
