Compromising Zammad Helpdesk - From Agent to Root
2022-07-21
Last year, I spent some time digging into Zammad , the open-source help desk platform, and reported three separate vulnerabilities to the maintainers. All three were fixed in Zammad 4.1.1 / 5.0.0, and the maintainers were responsive and handled disclosure well. I’m publishing the details now, long after the fixes shipped, because the full chain is a good illustration of how “medium severity in isolation” bugs compound into “an Agent-level user can get a shell on your server.”
TL;DR
Starting from nothing but a low-privilege Agent account (the kind you’d hand to any support rep), it was possible to:
- Escalate to Admin via a broken authorization check on the user-update API endpoint.
- Get remote code execution on the host by abusing the admin package-installer to write arbitrary files anywhere on disk - including
~/.ssh/authorized_keysand~/.bash_profile. - Pivot around internal networks using an SSRF in the GitHub/GitLab integration test, which could have been used earlier in the chain for reconnaissance, or independently for blind internal port-scanning.
CVE-2021-42086 Agent → Admin via a missing authorization check
Zammad’s user-update endpoint didn’t verify that the requesting user actually had permission to modify the target user. A regular Agent account - no special permissions required beyond the default role - could send a PUT to /api/v1/users/3 and simply overwrite the admin account’s login and password.
Two things made this reliably exploitable rather than theoretical:
- Zammad’s setup flow means the admin account is almost always user ID 3 (the first two IDs are reserved/system accounts), so there was no guessing involved.
- All you needed was a valid session cookie and CSRF token from your own Agent login - captured from your own browser, no trickery required.
The request looked like this:
PUT /api/v1/users/3
Host: zammad-a
Content-Type: application/json
X-CSRF-Token: <agent's own CSRF token>
Cookie: _zammad_session_a138cfd0f37=<agent's own session cookie>
{"login":"hacked-admin@zammad.com","firstname":"hack1","lastname":"err",
"email":"hacked-admin@zammad.com","password":"ZamPass123",
"role_ids":["1","2"],"group_ids":{"1":[],"2":"full"},
"active":true,"id":3}
A 200 response back, and logging in as hacked-admin@zammad.com / ZamPass123 gave full administrative control of the instance.
Worth noting: the bug only triggered when the admin account also had the ticket.customer permission - a fairly common configuration - which is presumably what let the update pass whatever partial authorization checks did exist.
There’s also a fun secondary edge case buried in the same code path: because the admin-creation logic special-cases “the first user created,” any external, unauthenticated visitor who reached a completely fresh Zammad instance (before an admin had signed up) could register and become admin themselves - a nice reminder that setup-wizard states deserve just as much scrutiny as steady-state auth logic.
Impact: any Agent - the lowest meaningfully-privileged role in the system - could become the instance administrator with a single crafted request.
CVE-2021-42094 Admin → Host RCE via the package installer
Once you have admin (via CVE-2021-42086, or legitimately), Zammad supports installing add-on packages through the UI by uploading a JSON manifest. The manifest lists files and their target locations, base64-encoded content included. The installer intended to keep those file writes confined to the application directory - but it didn’t sanitize ../ sequences in the location field.
A minimal malicious package:
{
"name": "backdoor",
"version": "1.0",
"vendor": "NastyInsider",
"files": [{
"location": "../../../../../../../../../opt/zammad/hacker_test_file",
"content": "WW91R290SGFjazNkIQ=="
}]
}
Upload that through Packages → Install Package, and Zammad happily wrote the decoded content to /opt/zammad/hacker_test_file - well outside the app’s own directory tree.
From arbitrary file write, there are several well-trodden roads to full code execution as the zammad user:
- Drop an SSH key. Write (or append) to
~/.ssh/authorized_keysfor the Zammad service account, and you have persistent, key-based SSH access to the host. - Hook the shell. Overwrite
~/.bash_profileso that the next time anything spawns an interactive login shell as that user, your payload runs - a classic path to a reverse shell. - Beyond that: dropping cron jobs, tampering with or deleting application logs to erase evidence of the intrusion, or simply causing denial of service by clobbering critical files.
How the vendor responded
This is the part of the story I think is actually more interesting than the bug itself. Zammad’s initial reply was, in essence: we know - a prior security audit had already flagged this, but their position was that package installation is inherently “remote code execution by design,” since add-ons are meant to extend the application and can legitimately ship arbitrary code.
I pushed back on that framing for a few reasons:
- Uploading a package is not the same event as executing it - installation and activation are (or should be) separable steps, and treating them as identical removes an opportunity for detection tooling to catch something malicious before it runs.
- There was no warning anywhere that granting
admin.packagewas equivalent to granting host-level code execution. That’s a “secure by default” / least-surprise problem independent of whether the underlying capability is “intentional.” - Chained with CVE-2021-42086, this meant a plain Agent - not even a legitimate admin - could ride the privilege-escalation bug straight into host compromise.
- There’s also the harder question of an already-compromised-but-undetected instance: without a security advisory, defenders have no way to know they should be hunting for this specific indicator of compromise in their environment.
To their credit, the maintainers reconsidered. The eventual fix, shipped with 5.0:
- Package installation can no longer write files outside the Zammad application directory, full stop.
admin.packagewas disabled by default, with the security implications of enabling it documented.
CVE-2021-42091 SSRF via the GitHub/GitLab integration test
The last piece: Zammad’s GitHub/GitLab integration lets an admin enter a host to connect to, and offers a “test connection” feature that reports back whether the request succeeded. That’s normal UX - except the app would happily attempt the connection to any address you supplied, including localhost and RFC1918 private ranges, and it told you enough about the result to distinguish “closed port,” “wrong protocol,” and “something’s listening here.”
Pointed at http://localhost:22/api/graphql, for instance, the response leaked the SSH banner - which is a textbook SSRF fingerprinting primitive. From there it was straightforward to script a basic internal port scanner: swap in a session cookie/CSRF token from an account with Integrations access, target an IP and port range, and let the integration endpoint act as your scanning proxy into a network segment you otherwise had no route to.
Why this matters beyond “an admin can port-scan localhost”:
- Organizations often assume that a web app account only gives access to that app, not to internal network segments behind it. SSRF quietly breaks that assumption, and does so through a channel (an “integration test”) that looks completely benign in logs.
- Because responses distinguished error types, this wasn’t just blind SSRF - it was enough to fingerprint services, which is a meaningfully worse outcome.
- No rate limiting meant it doubled as a free internal denial-of-service tool against anything reachable from the Zammad host.
- Chained with Vuln 1, an ordinary Agent - not even a real admin - could use this for internal reconnaissance before or after escalating.
Suggested mitigations (also what I passed along to the maintainers): stop returning granular failure detail from the connection test (this alone doesn’t eliminate SSRF, but kills the easy fingerprinting), and validate/allowlist target hosts server-side - blocking loopback, link-local, and private ranges outright rather than relying on the UI to discourage bad input.
Putting the chain together
Individually, these read as “medium-to-high” bugs with some caveats each. Chained, the story is much starker:
Agent account (default permissions)
│
├─▶ [CVE-2021-42086] Broken auth on PUT /api/v1/users/3
│ └─▶ Full Admin account
│
├─▶ [VE-2021-42094] Admin uploads malicious package (path traversal)
│ └─▶ Arbitrary file write as the zammad user
│ └─▶ authorized_keys / .bash_profile
│ └─▶ Shell on the host
│
└─▶ [CVE-2021-42091] SSRF via Integrations test (available at Agent or Admin level)
└─▶ Internal recon / port scanning from the compromised host
Someone who started with nothing more than a support-desk login could, in principle, have ended up with a persistent SSH foothold on the server and a scanning platform into whatever internal network that server sat on - without ever touching a genuinely “admin” credential that wasn’t of their own making.