Zammad CVE-2026-102489/CVE-2026-102490: Exploited Chain Hunt

Threat Overview

The Dutch Institute for Vulnerability Disclosure (DIVD) disclosed on September 30, 2026 that an intruder used two previously unknown Zammad vulnerabilities to breach DIVD on September 21. CVE-2026-102489 is a session-hijacking flaw that leads to remote code execution as the local zammad service user. CVE-2026-102490 is a local privilege-escalation flaw that allows the zammad user to become root. DIVD states that the attacker chained the flaws in seconds, then reached other services and exfiltrated volunteer email addresses and possibly contact details.

DIVD characterizes the intrusion as fast, automated, loud, and messy and assesses that an agentic AI system selected follow-on actions. That characterization is a victim/researcher assessment; the confirmed security facts are exploitation of the two Zammad flaws, execution as zammad, root escalation, access to additional services, and data exfiltration. DIVD has not publicly disclosed the exploit endpoints or complete attacker commands. Accordingly, this hunt emphasizes the high-signal process boundary from the Zammad application to a shell, the service-user-to-root transition, and post-exploitation activity rather than inventing URI or payload signatures.

References

Impacted Systems

  • Vendor/product: Zammad GmbH Zammad helpdesk/ticketing platform on Linux or Docker; self-managed deployments.
  • CVE-2026-102489: Zammad 6.3.0–6.5.4 is remotely exploitable through session hijacking leading to code execution as zammad. DIVD reports the vulnerable code is present in 7.0.0–7.1.3 but is not exploitable there because of environmental conditions.
  • CVE-2026-102490: Local privilege escalation from the zammad user to root; the CNA record lists Zammad 1.5.0 through versions before 7.1.0-alpha. DIVD’s case page advises upgrading to Zammad version 7 and marks a patch available.
  • Exposure/prerequisite: CVE-2026-102489 requires attacker reachability to the Zammad application and passive user interaction in the CVSS chain scenario. CVE-2026-102490 requires local access as the low-privileged zammad user; it becomes remotely consequential when chained after CVE-2026-102489.
  • Deployment role: Internet-facing Zammad web/application node; impact extends to credentials, integrations, and adjacent services reachable from that node.
  • Remediation status: Upgrade to the current Zammad 7 release recommended by DIVD/Zammad. The public case does not identify a single granular fixed build for both conditions; validate the exact release against current vendor guidance before change execution.

Why this matters

The chain is confirmed exploited and crosses from unauthenticated/session-level access to application RCE and then root. Zammad often holds customer communications, attachments, email integration credentials, OAuth/API tokens, and links to identity and collaboration systems. The incident also demonstrates rapid automated follow-on activity, reducing the time between initial access and host-wide compromise.

Exploitation Status

Confirmed exploitation in one publicly documented breach. DIVD records first access on September 21, identified the two zero-days during incident response, and began scanning and notifying vulnerable public instances on September 26. The public evidence does not establish mass exploitation, an attributed threat actor, or a reliable AI-agent fingerprint. No exploit URI, stable payload string, attacker IP list, or complete command sequence has been released in the cited case material.

What this hunt looks for

Zammad or Ruby web processes spawning shells, execution as the zammad account, rapid transition to root, download and execution tooling, persistence changes, rare outbound connections, new successful POST sources, and archive staging.

Required logs

Microsoft Defender for Endpoint or equivalent Linux telemetry providing DeviceProcessEvents, DeviceFileEvents, and DeviceNetworkEvents; Linux Syslog or audit logs; reverse-proxy/WAF telemetry in CommonSecurityLog; and container runtime logs for Docker deployments.

Hunt 1 — Zammad web application spawning a shell or command interpreter

let lookback = 30d;
DeviceProcessEvents
| where Timestamp >= ago(lookback)
| where FileName in~ ("sh", "bash", "dash", "zsh", "ksh")
| where InitiatingProcessAccountName =~ "zammad" or InitiatingProcessCommandLine has_any ("zammad", "puma", "passenger", "rails") or InitiatingProcessFileName in~ ("ruby", "puma", "passenger")
| project Timestamp, DeviceName, AccountName, FileName, FolderPath, ProcessCommandLine, InitiatingProcessAccountName, InitiatingProcessFileName, InitiatingProcessFolderPath, InitiatingProcessCommandLine
| order by Timestamp desc

Hunt 2 — Root execution immediately following zammad-user activity

let lookback = 30d;
let serviceActivity = DeviceProcessEvents
| where Timestamp >= ago(lookback)
| where AccountName =~ "zammad" or InitiatingProcessAccountName =~ "zammad"
| project DeviceId, ServiceTime=Timestamp, ServiceProcessId=ProcessId, ServiceCommand=ProcessCommandLine;
DeviceProcessEvents
| where Timestamp >= ago(lookback)
| where AccountName =~ "root"
| join kind=inner (serviceActivity) on DeviceId
| where Timestamp between (ServiceTime .. ServiceTime + 5m)
| project RootTime=Timestamp, DeviceName, FileName, ProcessCommandLine, InitiatingProcessFileName, InitiatingProcessCommandLine, ServiceTime, ServiceProcessId, ServiceCommand
| order by RootTime desc

Hunt 3 — Download, decode, or execute tooling under the zammad account

let lookback = 30d;
DeviceProcessEvents
| where Timestamp >= ago(lookback)
| where AccountName =~ "zammad" or InitiatingProcessAccountName =~ "zammad"
| extend Cmd = tolower(ProcessCommandLine)
| where FileName in~ ("curl", "wget", "nc", "ncat", "socat", "python", "python3", "perl", "ruby", "base64", "openssl") or Cmd has_any ("/dev/tcp/", "base64 -d", "chmod +x", "nohup", "setsid", "mkfifo")
| project Timestamp, DeviceName, AccountName, FileName, FolderPath, ProcessCommandLine, InitiatingProcessFileName, InitiatingProcessCommandLine
| order by Timestamp desc

Hunt 4 — Privilege and persistence changes by zammad or its descendants

let lookback = 30d;
DeviceProcessEvents
| where Timestamp >= ago(lookback)
| where AccountName =~ "zammad" or InitiatingProcessAccountName =~ "zammad" or InitiatingProcessCommandLine has "zammad"
| extend Cmd = tolower(ProcessCommandLine)
| where Cmd has_any ("/etc/sudoers", "useradd", "usermod", "passwd", "authorized_keys", "systemctl enable", "daemon-reload", "crontab", "/etc/cron", "setcap", "chmod u+s", "chown root")
| project Timestamp, DeviceName, AccountName, FileName, ProcessCommandLine, InitiatingProcessAccountName, InitiatingProcessFileName, InitiatingProcessCommandLine
| order by Timestamp desc

Hunt 5 — Executable or script creation by Zammad web/service processes

let lookback = 30d;
DeviceFileEvents
| where Timestamp >= ago(lookback)
| where InitiatingProcessAccountName =~ "zammad" or InitiatingProcessCommandLine has_any ("zammad", "puma", "passenger", "rails")
| where ActionType in~ ("FileCreated", "FileModified", "FileRenamed", "Created", "Modified", "Renamed")
| where FileName endswith ".sh" or FileName endswith ".py" or FileName endswith ".pl" or FileName endswith ".rb" or FileName endswith ".so" or FileName endswith ".service" or FolderPath has_any ("/tmp/", "/var/tmp/", "/dev/shm/", "/etc/cron", "/etc/systemd/system", "/root/.ssh")
| project Timestamp, DeviceName, ActionType, FolderPath, FileName, SHA256, InitiatingProcessAccountName, InitiatingProcessFileName, InitiatingProcessCommandLine
| order by Timestamp desc

Hunt 6 — Rare outbound connections initiated by Zammad or shell descendants

let baseline = 30d;
let recent = 2d;
let known = DeviceNetworkEvents
| where Timestamp between (ago(baseline) .. ago(recent))
| where InitiatingProcessAccountName =~ "zammad" or InitiatingProcessCommandLine has_any ("zammad", "puma", "passenger")
| summarize by DeviceId, RemoteIP, RemoteUrl;
DeviceNetworkEvents
| where Timestamp >= ago(recent)
| where InitiatingProcessAccountName =~ "zammad" or InitiatingProcessCommandLine has_any ("zammad", "puma", "passenger") or InitiatingProcessFileName in~ ("sh", "bash", "curl", "wget", "nc", "ncat", "socat")
| summarize FirstSeen=min(Timestamp), LastSeen=max(Timestamp), Connections=count(), Ports=make_set(RemotePort, 20), Processes=make_set(InitiatingProcessFileName, 20), Commands=make_set(InitiatingProcessCommandLine, 10) by DeviceId, DeviceName, RemoteIP, RemoteUrl
| join kind=leftanti known on DeviceId, RemoteIP, RemoteUrl
| order by LastSeen desc

Hunt 7 — New external sources performing successful POST activity to Zammad

let baseline = 30d;
let recent = 2d;
let knownSources = CommonSecurityLog
| where TimeGenerated between (ago(baseline) .. ago(recent))
| where RequestMethod =~ "POST"
| summarize by DestinationIP, SourceIP;
CommonSecurityLog
| where TimeGenerated >= ago(recent)
| where RequestMethod =~ "POST"
| where DeviceAction !in~ ("blocked", "deny", "dropped")
| summarize FirstSeen=min(TimeGenerated), LastSeen=max(TimeGenerated), Requests=count(), Urls=make_set(RequestURL, 20), UserAgents=make_set(RequestClientApplication, 10) by DestinationIP, SourceIP
| join kind=leftanti knownSources on DestinationIP, SourceIP
| order by Requests desc

Hunt 8 — Archive creation or data staging after zammad/root execution

let lookback = 30d;
DeviceProcessEvents
| where Timestamp >= ago(lookback)
| where AccountName in~ ("zammad", "root") or InitiatingProcessAccountName =~ "zammad"
| extend Cmd = tolower(ProcessCommandLine)
| where FileName in~ ("tar", "zip", "7z", "gzip", "mysqldump", "pg_dump", "rsync", "scp", "rclone") or Cmd has_any ("/var/lib/zammad", "storage/", "backup", "dump", "attachments")
| project Timestamp, DeviceName, AccountName, FileName, ProcessCommandLine, InitiatingProcessAccountName, InitiatingProcessFileName, InitiatingProcessCommandLine
| order by Timestamp desc

Hunt 9 — Syslog fallback for exploitation and privilege escalation

let lookback = 90d;
Syslog
| where TimeGenerated >= ago(lookback)
| where SyslogMessage has_any ("zammad", "puma", "passenger")
| where SyslogMessage has_any ("sudo", "COMMAND=", "session opened for user root", "useradd", "authorized_keys", "curl", "wget", "chmod +x", "systemctl enable", "crontab")
| project TimeGenerated, Computer, HostName, Facility, SeverityLevel, ProcessName, SyslogMessage
| order by TimeGenerated desc

Detection Notes

  • Hunt 1 is the highest-value generic exploitation-boundary hunt. Zammad administrative jobs may legitimately invoke scripts, so confirm unexpected parentage, account context, command content, and timing.
  • Hunt 2 is intentionally temporal because the exact local privilege-escalation mechanism is undisclosed. It can be noisy on busy hosts; process-tree identifiers or audit session IDs should replace the time join when available.
  • DIVD has not released an exploit URI or stable payload signature in the cited public case. Hunt 7 detects new successful POST sources but cannot by itself identify CVE-2026-102489.
  • ruby, puma, and passenger are common legitimate application components. Shell creation, download tooling, new executable writes, or rare egress from that lineage materially raises confidence.
  • Containerized Zammad can shift process and file visibility to the container runtime. Defender tables require a supported sensor on the relevant host; otherwise use container audit/runtime logs normalized into Sentinel.
  • Reverse-proxy access logs do not prove command execution. Conversely, direct-to-application traffic may bypass a WAF and never appear in CommonSecurityLog.
  • No public IOC list was reproduced in this package because the vendor’s linked log-check script could not be validated as static report content. Use behavior and authoritative vendor tooling without converting undisclosed strings into claims.