Unitrends是用于任何IT环境的一体式企业备份软件,配有网络服务器界面来管理网络。虽然许多专有信息被模糊化为PHP共享对象库,但是当登录到Unitrends提供的试用虚拟机(VM)时,可以查看Web服务器文件,这些安装的默认密码是unitrends1
。这里将详细介绍我在Unitrends应用程序中发现的几个漏洞,包括三个远程执行代码的漏洞。
寻找漏洞时,我们一般遵循着一条公式:
在寻找关键的函数调用时,由于Web应用程序是用PHP编写的,我们将开始搜索可以在系统上远程执行代码的功能。从php文档中,这包括exec,shell_exec,system,passthru,pcntl_exec,popen
和backtick
操作符。如果任何用户输入推送这些函数调用中的一个是未过滤的或过滤不好的,则机器上的代码执行将成为可能。这里我们使用find
和grep
来追踪这些被调用的位置。
从查找结果中修改输出。
[user@localhost]$|-name "*.php" -exec bash -c 'grep -H -E "((shell_|\ |pcntl_)exec\()|(\ system\()| (\ passthru\()|(\ popen\()" $1' - {} \; | tr -s [:space:]
从上图可以看出,有一些有趣的文件/api/includes/system.php
和/api/includes/restore.php
。对于systems.php
,似乎是通过popen
函数来发出密码更改的,而restore.php
看起来正在进行一些文件管理。
我们要审查这两个文件,现在我们要更仔细地分析systems.php
文件。
从上面的输出中,我们看到了system.php
文件。在文件中,我们将看到一个名为change_system_password
的函数,其中包含以下代码:
有问题的函数是api/includes/systems.php
。如果webserver是root(ala$res=trim(shell_exec('echo$UID')))
,那么系统信任用户传递为有效。由于Web应用程序通过passwd
进行用户管理,因此需要root
才可以更改其他用户的密码。
从注释中可以看到,此函数通过popen
命令处理系统用户(在/etc/passwd
中找到的密码)的更改密码。因为这可以从Web应用程序界面完成,所以当Web服务器以root
身份运行时会出现问题(在Unitrends 9.0.0设备之前的情况)。你可以通过一个恶意用户如`sleep 10
`,让它发出POPEN
参数“/usr/bin/passwd –stdin sleep 10
”。由于在更改密码之前执行了反引号语句,因此服务器会休眠十秒钟。
要查找此函数调用的位置,我们使用上面的grep
来搜索对change_system_password
进行的调用,并发现它直接从/recoveryconsole/bpl/password.php
调用。
该文件如下所示:
require 'header.php'; $passwordScript = $rootdir . '/system/password_change.php'; require $passwordScript; $user = isset($_REQUEST['user']) ? $_REQUEST['user'] : 'root'; $currentPassword = $_REQUEST['password']; $newPassword = $_REQUEST['newpassword']; $errorMessage = ""; $result = change_system_password($user, $currentPassword, $newPassword, $errorMessage);
该脚本只是抓取由$ _REQUEST
变量发出的变量,并将它们传递给我们的漏洞函数。这里唯一需要注意的是在header.php
文件中,它依赖于一个“AuthToken
”头来传递请求。该令牌在首次登录时发出,并设置在“token
”cookie中。剩下的只是使用AuthToken
头与必需的$_REQUEST
变量来编写请求并利用应用程序。下面显示了一个围绕这个过程的python
包装类,来使webserver
请求我们的攻击机器。
由于更改系统密码的功能中有一个严重的漏洞,所以我决定更仔细地了解Web应用程序里面用户的更改密码功能。密码管理是在用户类模型中完成的,这表明当向该类发出put请求时,我们就可以利用了。以下是Users
类模型中的漏洞代码段。主要是使用’force
‘参数提交PUT请求设置密码,绕过正常的访问控制。
public function put($which, $data) { . . . misc variable init . . . if (isset($data['password'])) { if(isset($data['force']) && $data['force'] === true){ $userArray['password'] = $data['password']; } else if (isset($data['current_password'])) { $valid = $this->BP->authenticate($userName, $data['current_password']); if ($valid !== -1) { $userArray['password'] = $data['password']; } else { $msg = "Incorrect password."; } } else { $msg = "Specify the current password."; } }
为了利用我们的漏洞代码,我们通过为“新用户”打补丁,寻找Users类模型的每个新实例。这样做在/api/includes/appliance.php
中只产生一个只有四个结果的文件。在文件中,声明了一个函数:
public function update_users($which, $data) { require_once('users.php'); $users = new Users($this->BP); return $users->put($which, $data); }
接着我们要调用“update_users
”,这是从菜单的/api/index.php
文件中完成的。按照API索引文件的开关控制流程,我们可以通过向/api/users/$UID/
发出PUT请求来更改由$UID密码标识的用户。
开始查找无限制文件上传的一种方法是以不安全的方式查找文件写入。我们做一个类似的find
和grep
,注意到以下结果:
~/scratch/html$ find . -name "*.php" -exec bash -c 'grep -H "fopen(" $1 | tr -s [:space:]' - {} \; ./grid/portal/rflr_manage.php: $log = fopen("/usr/bp/logs.dir/$logname.log", "w"); ./recoveryconsole/bpl/logger.php: $fp = fopen($file, "w+"); ./recoveryconsole/bpl/reports.php: $fp = fopen($file, 'w+'); ./api/includes/logger.php: $fp = fopen($file, "w+"); ./api/includes/backups.php: $fp = fopen("/tmp/before", "a"); ./statussync/logger.php: $log_handle = fopen($log_filename, 'r'); ./statussync/logger.php: $handle = fopen($log_filename, 'a'); // open/create file for appending
在遍历这些路径中的每一个后,我们最感兴趣的是./recoveryconsole/bpl/reports.php
。调查fopen
,我们发现它是一个名为saveReport
的简单函数的一部分,它是一个文件和内容,并将内容写入文件。我们发现它使用以下代码从另一个switch/case scenario
方案调用saveReport
:
case "file": if (isset($_GET['report']) && isset($_REQUEST['contents'])) { $reportType = $_GET['report']; $contents = $_REQUEST['contents']; } else { $BP->buildResult($xml, false, "error: report type, and content needed for saving file."); echo($xml->getXml()); break; } $baseName = isset($_GET['name']) ? $_GET['name'] : 'report'; $reportDirectory = $BP->get_ini_value("Location Information", "Reports-Dir"); if ($reportDirectory === false) { // Since we are not erroring out, log in the error log. // Use the default value /usr/bp/logs.dir. global $Log; $message = $BP->getError() . " - Error retrieving ini field: Location Information, Report-Dir, using default (/usr/bp/reports.dir)."; $Log->writeError($message, true); $reportDirectory = "/usr/bp/reports.dir"; } $fileName = createReportName($baseName, $reportDirectory, $reportType); $bSuccessful = saveReport($fileName, $contents); if ($bSuccessful === true) { $xml->push("root"); $xml->element("ReportFile", $fileName); $xml->pop(); } else { $errorString = "Error saving report file '" . $fileName . "'."; $BP->buildResult($xml, false, $errorString); } echo($xml->getXml()); break;
这里有一些注意事项。这里的第一件事是用户控制变量$reportType
,$contents
和$basename
变量。这意味着我们可以控制在saveReport
中写入的内容。文件写入的“where
”由createReportName
函数确定,它显示如下:
function createReportName($baseName, $directory, $type) { $sName = $directory . '/' . $baseName; $timestamp = time(); $date = date('mdy-His', $timestamp); $sName .= $date . '.' . $type; return $sName; }
对于createReportName
,我们控制$baseName
和$type
变量。即使我们不拥有$
目录(因为$baseName
没有为坏字符进行审查),我们可以控制写入的目录。如果我们将$baseName
作为
../../../../../../../../../../../../../../../../../var/www/html/tempPDF/test1234
的
$type
变量“php
”,这将创建文件名:
$directory . ‘/’ . ../../../../../../../../../../../../var/www/html/tempPDF/test1234$date.php
。
然后它会将$contents
变量(我们控制的)写入新创建的PHP文件中,从而导致代码执行。tempPDF
目录文件写入的原因是该目录在Web服务器代理程序中的权限是可访问和可写的。请求完成后,我们将在XML
文档中查看返回给我们的文件,以便我们直接访问。读取reports.php
的来源显示,直接发送我们的GET
请求到PHP文件就可以利用此功能了,并且不需要其他api回溯跟踪。
该URL显示正在进行的查询参数,除了内容变量。当请求完成时,我们看到我们的报告文件被输出到tempPDF
目录中。剩下的要做的事情解释浏览页面并执行代码。
为此有人写了一个python包装类的漏洞利用,如下所示。
这个bug是通过模糊Unitrends
参数发现的,没有审查源代码。登录应用程序时有一些异常行为; 其中之一是当用户连接到Unitrends Web
界面时,只要你的请求来自该IP地址,会话令牌将无限期地生效。这导致我们对发布的“token
”cookie
进行深入调查,该cookie
用于与应用程序的身份验证。该cookie
必须进行URL解码,base64解码以显示几个冒号分隔值。例如:
v0:aa898f83-d85a-4599-b222-02ce513cbf25:1:/usr/bp/logs.dir/gui_root.log:0
在弄清这个cookie
格式之后,我们确定格式大体如下:
v0:$ SESSION_KEY:$ UID:$ LOG_FILE_PATH:$ LOG_LEVEL
下一个合乎逻辑的步骤是登录一个低级帐户(没有任何权限),看看我们是否可以升级到root。实际上,当你将UID
更改为1时,base64/URL
编码会重新提交你的Cookie,现在该帐户可以从Web界面访问root了。
附注:日志文件可以被操纵以写入任意文件并进行文件覆盖,但我们没有发现它是如何导致代码执行的。
在本系列的第二章中,我们将分析另一个远程代码执行漏洞,我们将逐步介绍如何利用漏洞,概述在support.unitrends.com(泄漏客户信息)域中的缺陷。
*作者:rhinosecuritylabs,