We discovered an interesting CTF-inspired vulnerability, CVE-2026-22200, in osTicket, a popular open source helpdesk system. This flaw allows anonymous attackers to read arbitrary files from the server by injecting malicious PHP filter chain expressions into a ticket and then exporting it to PDF. This can be exploited to exfiltrate sensitive files, embedded as bitmap images within the PDF, or achieve remote code execution when chained with CVE-2024-2961 (CNEXT). This issue is patched in osTicket 1.18.3 / 1.17.7, and we strongly encourage all users to upgrade to the latest version.
osTicket is a widely used open-source helpdesk system, favored by organizations seeking a lightweight, self-hosted support solution. At Horizon3.ai, we frequently encounter it within the SLED (State, Local, and Education) sector and other midmarket to SMB organizations. With thousands of instances exposed to the Internet and many more deployed internally, the attack surface is significant.
Ticketing systems tend to be high value targets for attackers. These systems typically contain sensitive information such as tokens or credentials, and may act as a breach point to pivot into internal networks. Recent vulnerabilities affecting ticketing systems that have been exploited in the wild include CVE-2024-28986/CVE-2024-28987 affecting Solarwinds Web Help Desk and CVE-2025-2775/CVE-2025-2776 affecting SysAid.
Architecturally, osTicket is an old-school PHP application. It was originally released in 2003 and has been researched extensively, including work by SonarSource and Checkmarx and others. Despite this prior scrutiny, as we surveyed the code base, the application’s reliance on old third-party libraries stood out as something that was worth exploring further. And given recent advancements in PHP filter chain exploitation, we saw an opportunity to apply a fresh lens.
We began by analyzing mPDF, the third-party PHP library osTicket uses to generate PDF documents from support tickets. This functionality is accessible to any user authorized to view a ticket, including unauthenticated guests if the helpdesk is configured for guest ticket access (the default).
PDF libraries are notoriously complex because they have to bridge the gap between two complex formats, HTML/CSS and PDF. A recurring point of failure in integrating these libraries into an application is the handling of external resources. It’s often unclear to what extent the calling application must sanitize URLs or local file paths before passing them to the PDF generator, and the generator itself could be buggy. This can lead to SSRF or local file read vulnerabilities.
While researching mPDF, we stumbled upon an interesting relevant CTF challenge, web2pdf, posed by @_splitline_ at HITCON CTF 2022, that explored how mPDF could be used to read arbitrary local files using an HTML fragment as simple as:
<img src="<malicious_url>"/>
Solving the challenge involved two tricks:
phar:// and php://. However, a bug in the library’s path handling allows an attacker to bypass this check using altered paths like php:\\ or ./php://. mPDF checks URLs against the stream wrapper blacklist prior to normalizing them. This bug still exists in the latest version of mPDF!/etc/passwd) as a valid image within the PDF. The sensitive data can then be extracted back from the resulting bitmap in the PDF file.The BMP trick is especially handy as it allows for exfiltrating files efficiently in one shot without using the slower error-based oracle method.
In applying the CTF solution to osTicket, the first obstacle we hit was that osTicket was using a really old version of mPDF from circa 2019. The normalization bypass from the web2pdf challenge simply wasn’t present in the version of mPDF embedded in osTicket.
However we found a different normalization bypass involving URL encoding. A URL encoded stream wrapper like php%3a// could bypass the blacklisted stream wrapper check. This is due to logic in the older version of mPDF that URL decodes local resources after checking them against the stream wrapper blacklist but before accessing them.
These decoded resources are then accessed later using file_get_contents:
Even with a bypass for mPDF, we still had to deliver our payload through osTicket’s input validation layer. All rich-text HTML content in tickets is cleaned and then processed by htmLawed, a third-party library used to purify input by neutralizing suspicious tags and attributes.
htmLawed strictly checks URI schemes using a whitelist-based approach and is smart enough to recognize URL encoded colons like %3a. An input URI like:
<img src="php%3a//myurl">
gets neutralized with a denied: prefix:
We ran into the same issue with other image attributes like srcset. All URIs in style attributes were also blocked outright. For instance a style attribute like
<ul><li style="list-style-image:url(http://myurl.com)">listitem</li></ul>
would also get neutralized with a denied: prefix:
While testing style attributes, we noticed a subtle parsing differential in htmLawed. If we included whitespace between the url keyword and the opening parenthesis, the URI escaped the sanitizer’s whitelist check.
For example, this payload survived htmLawed completely: <ul><li style="list-style-image:url (http://myurl.com)">listitem</li></ul>
However, this wasn’t an immediate win. mPDF follows strict CSS standards and expects url() without the extra space. If we left the space in, the exploit failed at the sink; if we took it out, htmLawed blocked the URI.
But as part of this testing we also noticed the output was curiously mangled.
<ul><li style="list-style-image:url (http">listitem</li></ul>
We found that osTicket registers a custom post-sanitization callback __html_cleanup with htmLawed that performed additional string manipulation on style attributes.
Well this is dangerous code because it’s being processed on already cleaned output from htmLawed. There are multiple transformations happening in this code including most significantly, HTML entity decoding and character stripping. Our challenge was now to come up with a payload that could survive htmLawed, survive the processing of key characters (quotes, semicolon, colon, etc) in __html_cleanup, and be transformed into something that would be accepted by mPDF. We came up with this payload:
<ul><li style="list-style-image:url"(php%3a//myurl)">listitem</li></ul>
This uses the HTML entity " for " to bypass htmLawed. This entity then gets decoded and stripped in __html_cleanup. Note that the entity doesn’t need the trailing semi-colon! In fact, using " breaks the parsing logic in __html_cleanup.
This is how the payload flows from source to sink:
url"(php%3a//myurl)url"(php%3a//myurl)__html_cleanup: url"(php%3a//myurl)__html_cleanup: url(php%3a//myurl)url(php://myurl)Now that we’ve got a working payload, let’s walk through the end-to-end exploit flow. We assume a default configuration of osTicket version 1.18.2 running on Ubuntu with e-mail set up. All referenced scripts are at https://github.com/horizon3ai/CVE-2026-22200.
To trigger the PDF export, an attacker must first be able to view a submitted ticket. In the default osTicket configuration, there are two paths for an anonymous attacker:
The brute force path is aided by the following:
In our testing, brute forcing is something that can be easily be accomplished in less than an hour over a standard Internet connection.
% python osticket_access_bruteforce.py http://osticket.example.com '[email protected]' --threads 20
======================================================================
osTicket Ticket Access Link Enumeration Script
======================================================================
Target: http://osticket.example.com/
Email: [email protected]
Ticket Range: 100000 - 999999
Delay: 0.5s
Threads: 20
[*] Scan started at: 2026-01-21 14:47:16
[i] Progress: 100/900000 tested, 0 valid found
[i] Progress: 200/900000 tested, 0 valid found
[i] Progress: 300/900000 tested, 0 valid found
>>TRUNCATED<<
[i] Progress: 27000/900000 tested, 0 valid found
[i] Progress: 27100/900000 tested, 0 valid found
[i] Progress: 27200/900000 tested, 0 valid found
[+] VALID: Ticket #127227 - Access link sent (email verification required)
[i] Progress: 27300/900000 tested, 1 valid found
[i] Progress: 27400/900000 tested, 1 valid found
[i] Progress: 27500/900000 tested, 1 valid found
>>TRUNCATED<<
Certain non-default but somewhat common settings make ticket access even easier:
"New Ticket: Ticket Owner" autoresponder is enabled, the system immediately emails a ticket access link to anyone who submits a new ticket.With access to a ticket, an attacker can now inject payloads targeting specific files on the server. In the example below, we generated a payload to exfiltrate /etc/passwd and the sensitive include/ost-config.php file. This malicious string is placed directly into the rich-text HTML content of the ticket.
If an attacker wants to target additional files after the ticket is already open, they can inject more payloads by replying to the ticket. However, the system processes ticket replies a little differently than ticket opens: it performs HTML entity decoding twice instead of once.
To account for this, the payload must be encoded again. Instead of using ", the payload requires the nested entity sequence &#34 to survive the double-decoding process and reach the mPDF sink in the correct format. Our osticket_ticket_payload_gen script handles this with the --reply flag.
Once the ticket contains the malicious HTML, the attacker navigates to the ticket view and “Print”s it to PDF. This forces the mPDF to process the injected list-style-image property, resolve the PHP filter chain, and render the target files.
The exfiltrated data is embedded as bitmap images within the generated PDF. These files can be extracted by stripping the forged BMP headers from the PDF’s image objects.
During our testing, we identified several nuances that impact the reliability of the exfiltration:
osticket_ticket_payload_gen script accounts for this by URL-encoding uppercase letters in the payload.osticket_ticket_payload_gen script provides these encoding options.What can an attacker do with an arbitrary file read in osTicket? Beyond standard system files like /etc/passwd, the primary target is the configuration file located at include/ost-config.php in the application web root.
# Encrypt/Decrypt secret key - randomly generated during installation.
define('SECRET_SALT','SEFDaIg1UP=Rh0xHE=Ij6Lew8u49L=Tt');
#Default admin email. Used only on db connection issues and related alerts.
define('ADMIN_EMAIL','[email protected]');
# Database Options
# ====================================================
# Mysql Login info
#
define('DBTYPE','mysql');
# DBHOST can have comma separated hosts (e.g db1:6033,db2:6033)
define('DBHOST','localhost');
define('DBNAME','osticket');
define('DBUSER','osticket');
define('DBPASS','XXXXXXXX');
This file contains credentials to the osTicket database and a SECRET_SALT value used for cryptographic operations. If the database happens to be exposed externally, an attacker can access it and dump all ticket data. Furthermore, the database password itself is a candidate for credential stuffing against other organizational accounts.
The SECRET_SALT is a master key that is used to encrypt/decrypt sensitive configuration such as LDAP credentials, SMTP credentials, and AWS access keys in the database. Note that, even if the database is not exposed externally, in versions of osTicket prior to 1.18.2, there’s a significant SQL injection vulnerability CVE-2025-26241 that enables any authenticated user (including self-registered users) to dump the contents of the osTicket database. When paired with this vulnerability, CVE-2026-22200, which provides access to the SECRET_SALT, an attacker can fully read the contents of the database.
The SECRET_SALT is also used to generate access links to tokens (described further below).
On Windows installations, the impact may extend even further; it is likely possible to access files on remote SMB shares on domain-joined computers and also leak the NTLM hash of the service account running osTicket via a forced authentication attempt.
The SECRET_SALT value also allows attackers to bypass authentication to gain access to tickets.
osTicket by default allows guest users to access tickets directly using an access link without signing in. There are two methods for generating this access link – we’ll walk through the older but still functional deprecated method that is easier to forge.
The deprecated access link is built off four components:
SECRET_SALT valueOutside of the SECRET_SALT value, other components of a ticket access link can be brute forced without hitting any rate limits.
If user self-registration is enabled (the default), the user registration endpoint acts as an oracle that will leak whether a user email has already been registered, allowing for username enumeration. The osticket_registered_user_enum.py script demonstrates how this can be done.
As described above, users and their associated external ticket numbers can be efficiently brute forced using the Check Ticket Access oracle and the osticket_access_bruteforce.py script.
Internal ticket ids are auto-incrementing identifiers starting at 1. Putting it all together, an attacker can forge an access link as follows:
% python3 osticket_forge_access_link.py 637963 140 '[email protected]' 'SEFDaIg1UP=Rh0xHE=Ij6Lew8u49L=Tt' http://osticket.example.com
[*] Calculating hash for ID: 140, Email: [email protected]...
[*] Calculated Hash (a): a32056617064315cae1b4d98a8c95772
[*] Sending GET request to: http://osticket.example.com/view.php
[*] Request Parameters: {'t': '637963', 'e': '[email protected]', 'a': 'a32056617064315cae1b4d98a8c95772'}
[+] Request sent successfully. Analyzing response...
--------------------------------------------------
Full URL Sent: http://osticket.example.com/tickets.php?id=140
Status Code: 200
In 2024, @cfreal_ discovered a brilliant heap-based buffer overflow vulnerability, CVE-2024-2961 (CNEXT), in glibc‘s iconv() function. We’re not going to cover the details here, but the gist of the vulnerability is that any PHP file read primitive could be transformed into a RCE. This was reportedly exploited in the wild in 2024 in conjunction with CVE-2024-34102, an unauthenticated XXE vulnerability affecting Adobe Magento. The same RCE chain is possible with CVE-2026-22200, as we’ll demonstrate below.
The original exploit at a high level requires knowledge of the PHP process memory layout, which can be obtained from the /proc/self/maps file, and the entire libc.so.6 binary from the target to calculate offsets accurately. However, as noted in the “File Read Quirks” section, the osTicket PDF generator limits exfiltration to approximately 45KB per “image.” So we modified the exploit to work with osTicket.
First, we use the file read primitive to read the /proc/self/maps and partial libc.so.6 file on the target, using zlib+base64 encoding.
After adding the payload as a reply to an existing ticket, we print the ticket to a PDF and extract the files.
Next we fingerprint the partial libc library using the NT_GNU_BUILD_ID and download the full libc from https://libc.rip/ using the pwntools library.
Then, using the full libc and the /proc/self/maps file, we generate a CNEXT payload to write a web shell to the application web root.
Then we add the CNEXT payload to an existing ticket and export it to PDF again to trigger the exploit. This will result in an Internal Server Error and the connection being reset, but the web shell will be available and the application will continue to function normally.
The end-to-end exploit sequence is a bit complex to automate, and while we could have vibe coded a one-shot exploit script, we were curious to see if Claude Code with Opus 4.5 could stitch together the different steps on its own. We set up a CTF against osTicket running in the default configuration with a randomly generated flag file set in the root directory. We gave Claude a prompt with a description of the vulnerability and the steps outlined in this blog post. We instructed Claude to only ask for help if it needed to access email.
Within 10 minutes, Claude was able to figure it out, only asking once for assistance to get the contents of a confirmation email after registering an account.
If you’re running an Internet-facing instance of osTicket, you should immediately patch to the latest osTicket version, 1.18.3 / 1.17.7 or later. The patch addresses CVE-2026-22200 by disabling PHP stream wrappers prior to invoking mPDF. If you’re running on osTicket on a Linux server, we also recommend checking and patching the server for CVE-2024-2961, which affects glibc versions <= 2.39.
If patching is not possible, the following mitigations can help prevent exploitation by anonymous attackers:
Admin Panel -> Users tab to disable public user self-registrationAdmin Panel -> Users tab to require registration and login to submit tickets.Admin Panel -> System tab to disable HTML in thread entries and e-mail correspondence.We’ve provided a check script, check.py that can be used to determine if you’re running an out of date osTicket version. This doesn’t test the exploit directly but checks for other changes that were part of the 1.18.3 / 1.17.7 update.
The following indicators are evidence of potential compromise:
GET and POST requests to the /login.php endpoint, indicative of brute force attempts to access tickets, potentially along with suspicious user agents like python-requestsGET /tickets.php?a=print&id=140php%3a// and convert.iconv, often resulting in 414 Request-URI Too Large errorsAs usual, as with any zero-day, we notified any exposed affected clients as part of our Rapid Response program and updated the NodeZero product with coverage.
To see how the NodeZero platform can help uncover and remediate critical vulnerabilities like this in your environment, visit our NodeZero Platform page or speak with an expert by requesting a demo.
Shout out to @_splitline_ for the web2pdf CTF challenge and the clever bitmap image trick, and @cfreal_ for the groundbreaking discovery of the CNEXT exploit chain.