Overview :
An unrestricted file upload vulnerability in keywordsImport.php in TestLink 1.9.20 allows remote attackers to execute arbitrary code by uploading a file with an executable extension. This allows an authenticated attacker to upload a malicious file (containing PHP code to execute operating system commands) to a publicly accessible directory of the application.



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). During a recent security audit, our AppSec team found an unrestricted file upload and two SQL Injection vulnerabilities. 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.


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.

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



FMS 5.9.5 Hotfix HFIX-314 (315091)