GestSup v3.1.15 multiple vulnerabilities

Disclosure timeline

Developers were contacted the 30/12/2021‌‌
Developers acknowledged receiving the vulnerabilities and asking to wait for two months so users may have the time to update‌‌
Disclosure on this blog was made the 06/03/2022

Remote Code Execution during installation

GestSup v3.1.15, when /install/ is available, allows an attacker to append arbitrary code in connect.php (contains database connection credentials) due to the lack of sanitization.

Extract of /install/index.php‌‌ from line 35 to 43

$_POST['server']=htmlspecialchars($_POST['server'], ENT_QUOTES, 'UTF-8');
$_POST['port']=htmlspecialchars($_POST['port'], ENT_QUOTES, 'UTF-8');
$_POST['dbname']=htmlspecialchars($_POST['dbname'], ENT_QUOTES, 'UTF-8');
$_POST['user']=htmlspecialchars($_POST['user'], ENT_QUOTES, 'UTF-8');
$_POST['password']=str_replace("';","",$_POST['password']);
$_POST['password']=str_replace('";','',$_POST['password']);
$_POST['password']=str_replace('system(','',$_POST['password']);
$_POST['password']=str_replace('$_GET','',$_POST['password']);
$_POST['password']=str_replace(';//','',$_POST['password']);

As we can see, the $_POST['password'] is not properly sanitized as it just consists of stripping several characters as well as the system(, $_GET, ;// strings in order to prevent code execution.‌‌‌‌

An attacker can bypass these checks by using another function than system() to execute code like passthru() and by passing commands using HTTP POST requests.

Exploit request, the password POST parameter is tainted with: d'.passthru($_POST['foobar']).'
The payload was written in connect.php
Execution of the id Linux command by calling connect.php

MySQL stacked query SQL injection

GestSup v3.1.15 is vulnerable to MySQL stacked query SQL injection. The /admin/lists/display.php does not sanitize the table GET parameter which results in SQL injection.

In the /admin/list.php file on lines 51 and 52, we can see that the $db_table is set and that HTML tags and single quotes are stripped from it which is not sufficient to prevent SQL injection.

$db_table=strip_tags($db->quote($_GET['table']));
$db_table=str_replace("'","`",$db_table);

This parameter is then passed to /admin/lists/display.php when it get loaded and is not processed before being added to the SQL query an be executed.

Extract of /admin/lists/display.php from line 204 to 229:

//define order
if($_GET['table']=='tassets_model'){$order='ORDER BY tassets_model.type,tassets_model.manufacturer ';} 
elseif($_GET['table']=='tcategory'){$order='ORDER BY number,service,name';} 
elseif($_GET['table']=='tsubcat'){$order='ORDER BY cat,name';} 
elseif($_GET['table']=='tcriticality'){$order='ORDER BY service,number';} 
elseif($_GET['table']=='tstates'){$order='ORDER BY number';} 
elseif($_GET['table']=='tpriority'){$order='ORDER BY number';} 
elseif($_GET['table']=='tassets_state'){$order='ORDER BY `order`';} 
elseif($_GET['table']=='tassets_network'){$order='ORDER BY `network`';} 
elseif($_GET['table']=='ttime'){$order='ORDER BY min';} 
else {$order='ORDER BY name';}

if($_GET['hide_disabled_values']){$disable=" AND disable='0' ";} else {$disable='';}

if($rright['dashboard_service_only']!=0 && $rparameters['user_limit_service']==1 &&  $_SESSION['profile_id']!=4){
	$where_service_list=str_replace('tincidents.u_service','service',$where_service);
	if($_GET['table']=='tsubcat') {
		$query="SELECT tsubcat.id,tsubcat.cat,tsubcat.name FROM `tsubcat`,`tcategory` WHERE tsubcat.cat=tcategory.id $where_service_list $disable ORDER BY tsubcat.name";
	} else {
		$query="SELECT * FROM $db_table WHERE 1=1 $where_service_list $disable $order";
	}
} else {$query="SELECT * FROM $db_table WHERE id!=0 $disable $order";} 

//build each line
if($rparameters['debug']) {echo 'QRY : '.$query;}
$query = $db->query($query);

In this piece of code, the value of the table GET parameter is used to define the ORDER BY part of the SQL statement. lastly, we can see that if we do not meet conditions of this portion of code, our parameter will be added in the final else in the following query :

SELECT * FROM $db_table WHERE id!=0 $disable $order
Injection of the Stacked Query
The MySQL database logs show that our injected query was properly executed

And Boolean-based blind SQL injection

GestSup v3.1.15 is vulnerable to a boolean-based blind SQL injection. The dashboard.php does not sanitize the order get parameter which results in SQL injection.

The order parameter is set. It is stripped from HTML tags and single quotes which does not seem to be sufficient to prevent SQL injection.
Extract of dashboard.php lines 167 and 168 :

$db_order=strip_tags($db->quote($_GET['order']));
$db_order=str_replace("'","",$db_order);

Extract of /dashboard.php from line 536 to 550

if(!$reload) //avoid double query for reload parameters in url optimization for large database
	{
		$masterquery = $db->query("
		SELECT SQL_CALC_FOUND_ROWS $select
		FROM $from
		$join
		WHERE $where
		ORDER BY $db_order $db_way
		LIMIT $db_cursor,
		$rparameters[maxline]
		"); 
	} else {$masterquery='';}
	$query=$db->query("SELECT FOUND_ROWS();");
	$resultcount=$query->fetch();
	$query->closeCursor();

As we can see, the parameter is set, then is directly used after HTML & single strippring.

After enabling the debug mode, we try to confirm that the order parameter is vulnerable.

Tainting the order parameter with an invalid table name raises an error.

Since the result of this query is not directly displayed in the page, we found that using a boolean-based blind payload would be the best way to confirm the vulnerability.

True statement: 1+AND+6501=(SELECT+(CASE+WHEN+(6501=6501)+THEN+6501+ELSE+(SELECT+9081+UNION+SELECT+9677)+END))--+-
False statement: 1+AND+6501=(SELECT+(CASE+WHEN+(6501=9999)+THEN+6501+ELSE+(SELECT+9081+UNION+SELECT+9677)+END))--+-
Injection of a statement that returns "True"
Injection of a statement that returns "False"

We can clearly a difference in the response size as the table is not displayed when we inject a statement that is evaluated to false.

Multiple Stored XSS

During our research, we identified several stored XSS in GestSup v 3.1.15. The /core/ticket.php does not properly sanitize the text and text2 GET parameters which results in the ability of storing HTML code in the database which is then displayed in /tickets.php page. This occurs during the edition of a ticket.

Extract of /core/tickets.php line 336 through 344:

//escape special char and secure string before database insert
$_POST['description']=$_POST['text'];
$_POST['resolution']=$_POST['text2'];

//remove <br> generate by IE browser
$_POST['description']=str_replace('<br><br><br>','',$_POST['description']);
$_POST['resolution']=str_replace('<br><br><br>','',$_POST['resolution']);
if($_POST['description']=='<br>'){$_POST['description']='';}
if($_POST['resolution']=='<br>'){$_POST['resolution']='';}

We can see that the sanitization here consists of removing <br>. We can then see from line 571 to 606 that a query is built using our initial text parameter which is now named $_POST['description'].

$qry->execute(array(
	'user' => $_POST['user'],
	'observer1' => $_POST['observer1'],
	'observer2' => $_POST['observer2'],
	'observer3' => $_POST['observer3'],
	'type' => $_POST['type'],
	'type_answer' => $_POST['type_answer'],
	'u_group' => $u_group,
	'u_service' => $u_service,
	'u_agency' => $_POST['u_agency'],
	'sender_service' => $_POST['sender_service'],
	'technician' => $_POST['technician'],
	't_group' => $t_group,
	'title' => $_POST['title'],
	'description' => $_POST['description'],
	'date_create' => $_POST['date_create'],
	'date_modif' => $_POST['date_create'],
	'date_hope' => $_POST['date_hope'],
	'date_res' => $_POST['date_res'],
	'priority' => $_POST['priority'],
	'criticality' => $_POST['criticality'],
	'billable' => $_POST['billable'],
	'state' => $_POST['state'],
	'user_validation' => $_POST['user_validation'],
	'user_validation_date' => $_POST['user_validation_date'],
	'creator' => $_SESSION['user_id'],
	'time' => $_POST['time'],
	'time_hope' => $_POST['time_hope'],
	'category' => $_POST['category'],
	'subcat' => $_POST['subcat'],
	'techread' => $techread,
	'techread_date' => $techread_date,
	'userread' => $userread,
	'place' => $_POST['ticket_places'],
	'asset_id' => $_POST['asset_id']
	));

On lines 746 to 758, we can see that the initial value of text 2 which is now named $_POST['resolution'] is inserted in the database.

//check your own ticket for update thread right
if($row['author']==$_SESSION['user_id']) 
{
	if($rright['ticket_thread_edit']) {
		$qry=$db->prepare("UPDATE `tthreads` SET `text`=:text WHERE `id`=:id");
		$qry->execute(array('text' => $_POST['resolution'],'id' => $_GET['threadedit']));
	}
} else {
	if($rright['ticket_thread_edit_all']) {
		$qry=$db->prepare("UPDATE `tthreads` SET `text`=:text WHERE `id`=:id");
		$qry->execute(array('text' => $_POST['resolution'],'id' => $_GET['threadedit']));
	}
}
POST request containing payloads in the text and text2 variables
The payload stored in text id executed in the victim's browser.
The payload stored in text2 id executed in the victim's browser.
Mastodon