TL;DR
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.
Background
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.
Vulnerability Breakdown
Starting with the Sink: mPDF
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:
- Normalization Bypass: mPDF attempts to blacklist dangerous URI schemes like
phar://andphp://. However, a bug in the library’s path handling allows an attacker to bypass this check using altered paths likephp:\\or./php://. mPDF checks URLs against the stream wrapper blacklist prior to normalizing them. This bug still exists in the latest version of mPDF! - PHP Filter Magic: To bypass mPDF’s image validation, PHP filter chains are used to prepend a valid Bitmap (BMP) header to the contents of an arbitrary file. This tricks mPDF into rendering arbitrary files (e.g.,
/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.
Snag 1: Different Version of mPDF
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:
Snag 2: HTML Sanitization
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:
Bypassing HTML Sanitization
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:
- Malicious input:
url"(php%3a//myurl) - htmLawed Output (untouched):
url"(php%3a//myurl) - Entity decoding in
__html_cleanup:url"(php%3a//myurl) - Character stripping in
__html_cleanup:url(php%3a//myurl) - URL decoding in mPDF:
url(php://myurl)
Putting It Together: Exploiting the PDF File Read
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.
Obtaining Ticket Access
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:
- Self-Registration: If enabled (the default), an attacker can simply register an account, log in, and open a ticket.
- Brute Force: If self-registration is disabled, an attacker can submit a ticket as a guest and then brute-force access via the “Check Ticket Status” form
The brute force path is aided by the following:
- Check Ticket Status Oracle: The Check Ticket Status form acts as an oracle and will confirm when the combination of an email address and ticket number is valid. If it’s valid, an access link to the ticket is sent to the user’s email.
- Small Ticket Number Space: By default the ticket number space is limited to 6 digits, starting with 100000 and ending with 999999
- Rate Limiting Bypass: Per-user rate limiting protection can be bypassed by simply opening a new session before each request. This also circumvents logging of brute force attempts.
- Multiple Tickets: An attacker can open multiple tickets (e.g. 100) to make brute forcing significantly faster since tickets numbers are distributed randomly throughout the 6-digit space.
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 'XXX@XXX.com' --threads 20
======================================================================
osTicket Ticket Access Link Enumeration Script
======================================================================
Target: http://osticket.example.com/
Email: XXX@XXX.com
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:
- Autoresponder: If the
"New Ticket: Ticket Owner"autoresponder is enabled, the system immediately emails a ticket access link to anyone who submits a new ticket. - UI Customization: If the ticket submission template has been modified to display the ticket number directly, no brute force is required at all.
Injecting Payloads into a 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.
Handling Ticket Replies
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.
PDF Extraction
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.
File Read Quirks
During our testing, we identified several nuances that impact the reliability of the exfiltration:
- Uppercase Character Sensitivity: We observed cases where file paths containing uppercase characters would fail to exfiltrate. Our
osticket_ticket_payload_genscript accounts for this by URL-encoding uppercase letters in the payload. - Encoding Variations: While standard filter chains work for text, binary files can be finicky. We found that wrapping the data in Base64 or zlib+Base64 filters before the BMP transformation yielded stable results for binary files. Our
osticket_ticket_payload_genscript provides these encoding options. - Size Limitations: Exfiltrated images generally appear to be truncated at the ~45KB mark. While this is more than enough to capture configuration files and credentials, it may limit the exfiltration of binaries, database files and large log files..
Impact of Arbitrary File Read
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','XXX@XXX.com');
# 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.
Forging Ticket Access
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:
- Internal ticket id (an auto-incrementing identifier)
- External ticket number (default 6 numbers)
- User e-mail
SECRET_SALTvalue
Outside 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 'XXX@XXX.com' 'SEFDaIg1UP=Rh0xHE=Ij6Lew8u49L=Tt' http://osticket.example.com
[*] Calculating hash for ID: 140, Email: XXX@XXX.com...
[*] Calculated Hash (a): a32056617064315cae1b4d98a8c95772
[*] Sending GET request to: http://osticket.example.com/view.php
[*] Request Parameters: {'t': '637963', 'e': 'XXX@XXX.com', 'a': 'a32056617064315cae1b4d98a8c95772'}
[+] Request sent successfully. Analyzing response...
--------------------------------------------------
Full URL Sent: http://osticket.example.com/tickets.php?id=140
Status Code: 200
Chaining File Read to RCE via CNEXT
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.
So Easy Claude Can Do It
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.
Remediation
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:
- Implement network or host firewall rules to restrict access to the osTicket server
- Update the osTicket configuration in the
Admin Panel -> Userstab to disable public user self-registration - Update the osTicket configuration in the
Admin Panel -> Userstab to require registration and login to submit tickets. - Update the osTicket configuration in the
Admin Panel -> Systemtab to disable HTML in thread entries and e-mail correspondence.
Vulnerability Detection
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.
IoCs
The following indicators are evidence of potential compromise:
- Web server access log containing a large volume of
GETandPOST requests to the/login.phpendpoint, indicative of brute force attempts to access tickets, potentially along with suspicious user agents likepython-requests - Higher volume than normal of tickets created or users registering accounts.
- Web server access log entries with a high volume of GET requests for printing tickets to PDF, e.g.
GET /tickets.php?a=print&id=140 - Web server access log entries containing GET requests with long paths containing PHP filter payloads containing strings like
php%3a// andconvert.iconv, often resulting in 414 Request-URI Too Large errors - Presence of web shell PHP scripts in the osTicket application web root
Timeline
- Aug. 28, 2025: Horizon3.ai reports PDF file read issue to EnhanceSoft over email with disclaimer about 90 day disclosure policy
- Aug. 29, 2025: EnhanceSoft acknowledges report
- Sept. 3, 2025: Horizon3.ai reports PDF file read issue can lead to RCE when exploited with CNEXT. Horizon3.ai discloses other moderate/low severity issues (stored XSS, SSRF, etc)
- Sept. 4, 2025: EnhanceSoft acknowledges additional information
- Sept – Dec. 2025: Multiple followups between Horizon3.ai and EnhanceSoft regarding the status of the patch.
- Jan. 12, 2025: After 130+ days since initial disclosure to the vendor, Horizon3.ai publicly discloses CVE-2026-22200 and notifies EnhanceSoft of public disclosure.
- Jan. 12 – Jan. 15, 2025: EnhanceSoft confirms the vulnerability and collaborates with Horizon3.ai to validate the fix.
- Jan. 15, 2025: EnhanceSoft releases patched version, 1.18.3 / 1.17.7
- Jan. 22, 2025: This blog post
As 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.
References
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.
- https://osticket.com/osticket-v1-18-3-v1-17-7-available/
- https://github.com/horizon3ai/CVE-2026-22200
- https://blog.splitline.tw/hitcon-ctf-2022/#%F0%9F%93%83-web2pdf-web
- https://blog.lexfo.fr/iconv-cve-2024-2961-p1.html
- https://github.com/ambionics/cnext-exploits/blob/main/cnext-exploit.py
- https://nvd.nist.gov/vuln/detail/cve-2026-22200
- https://nvd.nist.gov/vuln/detail/cve-2024-2961
- https://www.cve.org/CVERecord?id=CVE-2025-26241
