Numerous vulnerabilities in Faveo Helpdesk

6 min read

Faveo Helpdesk is an open source helpdesk and ticketing system. It uses the Laravel framework, and there are at least 170 active instances running and exposed to the internet.

It's built on Laravel 5.6 which was released in 2018 with 1 year of security support (which expired on 7/2/19).

Over the course of my investigation, I uncovered numerous vulnerabilities. I would suggest extreme caution when running Faveo exposed to the internet, as there are likely further undiscovered vulnerabilities.

Sensitive Data Exposure via Hard-coded Encryption Secret

In Faveo's default configuration, non-authenticated members can create tickets. When they create a ticket, they are provided with a URL (via email) that they can use to view, reply to, and modify that specific ticket.

The URL is in the following format, and contains an encrypted copy of the tickets numerical ID.

http://faveo-helpdesk.test/check_ticket/eyJpdiI6InZJdXVP....(more chars)......YThhYjlhMiJ9

This would usually be fine, since normally Laravel is configured with a randomly generated secure encryption key during installation. Unfortunately, in the case of Faveo, the installer does not generate a new key, so Laravel falls back to the hard-coded key in the configuration file.

Accessing a ticket not owned by the current Faveo user

Since the encrypted value is simply the integer id of the ticket, it's possible to write a simple script to enumerate all the tickets in the system. For example, the below script run in a fresh download of the Faveo source code will generate valid ticket URLs for every ticket.

foreach (range(1, 1000) as $ticketNumber) {
    $encryptedValue = Crypt::encrypt($ticketNumber);
    echo 'https://faveo-instance.com/check_ticket/' . $encryptedValue . "\n";
}

Fortunately, patching this vulnerability is as easy as changing the application's secret to a new, random value. Be aware this will invalidate all previous URLs, though.

RCE via Hard-coded Encryption Secret and Deserialization

After finding the bug above, I checked the code and noticed that user input is passed directly to the Crypt::decrypt() function. Inside the view checkticket.blade.php, the user input (the route param id) is passed directly to the decryption function.

$tickets = App\Model\helpdesk\Ticket\Tickets::where('id', '=', \Crypt::decrypt($id))->first();
$thread = App\Model\helpdesk\Ticket\Ticket_Thread::where('ticket_id', '=', \Crypt::decrypt($id))->first();

Looking at the function signature of decrypt(), if the second argument is not set to false, the decrypted content is passed to the unserialize function.

public function decrypt($payload, $unserialize = true)

By passing a malicious serialized payload (a generic gadget chain for Guzzle) encrypted with the hard-coded secret, we can force Faveo to decrypt and deserialize the payload. This causes the application to write a malicious file into the /public directory which we can execute by visiting the malicious PHP file.

// Uses the hardcoded encryption key in every Faveo install
$payload = encrypt(
    'O:31:"GuzzleHttp\Cookie\FileCookieJar":4:{s:41:"GuzzleHttp\Cookie\FileCookieJarfilename...',
    false // Don't serialize the payload (as it's already serialized)
);

// Generate URL with payload embedded
echo sprintf('https://faveo.example.com/check_ticket/%s', $payload);
// URL where the shell will be placed
echo 'https://faveo.example.com/shell.php';

As with the previous vulnerability, patching this is also easy by rotating the application's secret. In addition, passing false as the second argument to the decrypt function to disable deserialization prevents an attacker from exploiting the vulnerability even if the application's secret is compromised.

Insufficient Access Control on Admin Endpoints

In addition to the authorization bypass above, there are also multiple routes which are part of the administration area that have no access control applied. Any user, including a user who has just signed up via the public register form, is able to access and change settings in these pages.

The affected routes are:

  • /close-workflow - View and configure the ticket workflow settings
  • /security - Configure the application security settings, including authentication rate limiting.
  • /queue - Configure the application's queue settings. Notably, when the SQS queue is used this exposes the AWS access keys to any user.
  • /ticket/priority - Create and edit ticket priority levels
  • /storage - Configure the application's storage location for files

All of these endpoints should be accessible to administrator accounts only, and missing access control on these endpoints could be catastrophic.

Persistent XSS via Ticket Priority

While researching the severity of the exposed endpoints above, I noticed a stored XSS vulnerability in the ticket priority description field. When the value is rendered in the priority list, the value is not correctly escaped. This could be used to compromise admin authentication credentials and further compromise the system.

Stored XSS in Faveo Admin Panel

Example Request to persist a stored XSS below.

curl --location --request POST 'http://faveo-helpdesk.test/ticket/priority/edit1' \
--header 'Cookie: XSRF-TOKEN=eyJpdiI6IlU3VExTOHIwaDRvWWhnaVF2XC9kQ0pRPT0iLCJ2YWx1ZSI6Ikk3SmR2Y3FqQ0l5VjEzQWxIS0dGY2R0MkhXekQxZ2NvOEpkcjZmN2I1Y2YxRHZXa1l5elFaOTZXS0pBXC9CVnZXIiwibWFjIjoiZTM5OGI0NzU2NzZmNTIwYTZkMTY5ZThlYWNkZjVmYTg4OGY1OTEyMTliNGIyOGNmZTA2MDc5M2M5YmM2OTQ1YyJ9; laravel_session=eyJpdiI6IkoycW54dlJ3Zk1cLzcxUG1Sc0FCdlNBPT0iLCJ2YWx1ZSI6IlwvaG14UzBzbVl1OUdhdjg4dWVXNFBNUm5IeE5aZHhKRG9NUmhsMGI1UEFtTnpQQ1VnSWFBZTNWZDBpa1RaelYrIiwibWFjIjoiMjU3MGQ1OTA4NjkzZmE4MGY3NzZiMDA1Yzg0MDAxZTNlOWI3YWYzYWYxYTMyNzc4Mjg0MDVlOWMwMGZlOTg0NCJ9' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode '_token=MiK57ra9h6tyXSuINWKWT5ysg2rPHcvMbLDuhhZs' \
--data-urlencode 'priority_id=5' \
--data-urlencode 'priority=Test' \
--data-urlencode 'priority_desc=<script>alert('\''ticket+priority+xss'\'');</script>Desc' \
--data-urlencode 'priority_color=#b73c3c' \
--data-urlencode 'status=1' \
--data-urlencode 'ispublic=1' \
--data-urlencode 'admin_note=' \
--data-urlencode '='

This vulnerability is especially severe as any user can access this page due to the missing access control detailed in the section above.

RCE via Insecure Upload + Storage Reconfiguration

Since the storage configuration page is accessible to any user, it's possible to reconfigure the application to write to an arbitrary path. It's worth noting that Faveo appends /attachments to the end of your configured path as well.

Setting the upload path

To gain RCE, we can write a PHP file into the /public directory. PHP files in this directory will be executed by the web server, as long as they contain valid PHP code and use the .php extension.

First, we set the storage root to /public (which is helpfully included in the dropdown for us to select). We then create a new ticket, selecting our PHP shell, which is successfully uploaded and placed in the /public/attachments directory.

Uploaded shell

Normally, clicking the attachment here would securely read the file using PHP and return it to the visitor using the /image/{$id} route. Since we changed the upload path, Faveo placed the attachment in /public/attachments/8791.php with the following contents.

<?php echo file_get_contents('/etc/passwd');

Since the file is inside the web root, we don't need to use the /image route, and can directly execute the file. When we visit the file, we see the contents of /etc/passwd. This would allow the attacker full access to the Faveo instance, including extracting data from the database, or using other exploits to gain root privileges on the server.

Timeline

  • January 29, 2021: Report and POC sent to the Faveo maintainer (ladybirdweb).
  • January 29, 2021: Maintainer acknowledges the issues via email.
  • February 12, 2021: Maintainer reports that fixes for the issues are in progress.
  • May 10, 2021: CVE-2021-26487, CVE-2021-26488, CVE-2021-26489, CVE-2021-26490, CVE-2021-3296 assigned to the vulnerabilities.
  • June 7, 2021: Requested an update on progress from the maintainer before the disclosure date of June 30 (No response).
  • June 29, 2021: Offered maintainer an extra week before disclosure to patch issues (No response).
  • July 15, 2021: Blog post published.