Vtenext 25.02: A three-way path to RCE

Multiple vulnerabilities in vtenext 25.02 and prior versions allow unauthenticated attackers to bypass authentication through three separate vectors, ultimately leading to remote code execution on the underlying server.

Vtenext 25.02: A three-way path to RCE

Summary

  • Product: VTENEXT CRM
  • Vendor: vtenext
  • Severity: Critical
  • Impact: Authentication Bypass and Remote Code Execution
  • Affected Version(s): 25.02.01 and below
  • Tested Version(s): 20.04, 24.02, 25.02, 25.02.1
  • First Patched Version: 25.02.2

Preface and history of the research

TL;DR: In my spare time, I happen to research vulnerabilities in products or services that I have encountered in the past and have pinned down over time. In the last few months, I decided to take a closer look at a CRM solution used by a fair number of small and medium-sized Italian companies, named VTENext.

What is vtenext?

VTENext is an Italian openclosed-source CRM platform available both On-Premises and in the Cloud, that seamlessly integrates with a business process management (BPMN) engine to offer comprehensive automation across marketing, sales, post‑sales, and customer support workflows.

Although vtenext began as an open-source CRM, it has evolved into a commercially driven product, with limited open access compared to its origins.

At the time of writing, two different main versions exist:

The research uncovered several critical flaws that, when combined, allowed users to completely bypass the login mechanism, authenticate on behalf of another user and, in most situations, execute remote code on the underlying server.

Following the discovery, I tried multiple times to contact the vtenext team and developers, both through official and unofficial channels, but we were never successful:

  • On May 28th, we contacted vtenext for the first time through their official contact form on the site and by sending an email requesting a responsible disclosure process. We received a default message back, but no further contact was made.
  • On June 5th, we contacted vtenext for the second time. We received a default message back again, but no further contacts.
  • On July 13th, we attempted to contact the developers of vtenext via a direct channel on LinkedIn, but without success.

Around July 24th, 2025, vtenext released version 25.02.1, which included a security patch for the third (and most severe) authentication bypass vector mentioned in the blog post. A few days later, version 25.02.2 was released, including fixes for the other vectors.

As of this writing, the vulnerabilities mentioned in the blog post can no longer be exploited starting from vtenext 25.02.2.

August 13th update: After notification of the article's publication, the vtenext team responded:

"Unfortunately, previous communications sent from a Gmail address may have been marked as spam due to the sender's format (0xbro), and we therefore did not see the message. Some of the vulnerabilities reported have already been corrected recently, as they were detected during VAPT activities conducted by third parties that we commission periodically. We will contact you again as soon as we have the details of the resolution or for any further information, as we always do with independent researchers who write to us, as this is not the first time we have collaborated with freelance professionals. [...] The lack of response was not due to negligence, but to the circumstances described above"
Following a call with the vtenext team on September 4th, it was decided to reduce the technical details of the vulnerabilities to give users time to update to the latest version. Given the existence of some patches for critical vulnerabilities, however, we have decided to keep our research public. Our goal is to draw attention to the general software's security posture and highlight the importance of updating to the latest available version.

With access to the legacy codebase but also to the stable latest working environment, I adopted a diff-testing approach focusing on static code analysis and debugging on a local instance of the open-source version, while conducting practical exploitation and dynamic testing against the latest demo release.

An initial semgrep scan using default PHP rules gave the following results:

semgrep --dataflow-traces --force-color --matching-explanations
    --text-output=scans/$(date "+%Y%m%d").txt
    --sarif-output=scans/$(date "+%Y%m%d").sarif 
    --no-git-ignore 
    --config /opt/semgrep-rules/php/lang/security/

run semgrep with default PHP rules

semgrep scan result

Long story short, 1000+ semgrep code findings later, I had three different authentication bypass vectors as well as some potential code execution primitives (and a module-based code execution by design, but more on this later).

Oh… and there’s probably a lot more to be found. For now, however, let’s take a closer look at the three attack vectors 🔎.

Authentication Bypass: Vector #1

Both the first and second attack vectors necessitate user interaction for successful exploitation and rely on a sequence of multiple vulnerabilities that collectively enable the attack.

ℹ️
The vulnerability has been patched in version 25.02.2

The first vector involves an exploitation chain featuring the following:

  • Reflected Cross-Site Scripting (XSS) via POST request
  • CSRF token validation bypass via HTTP Method Tampering
  • Session Cookie Information Disclosure

Reflected Cross-Site Scripting (XSS) via POST request

reflected cross-site scripting vulnerability exists in the modules/Home/HomeWidgetBlockList.php because of two issues:

  • The widgetId keys contained in the widgetInfoList JSON arrays are reflected within the server response without proper sanitisation.
  • The JSON response is delivered with a Content-Type: text/html header instead of the correct Content-Type: application/json, which allows the browser to interpret and execute embedded JavaScript or HTML content.
Sample HomeWidgetBlockList HTTP request
widgetId reflected in HTTP response

As a result, this combination enables the injection and execution of arbitrary JavaScript code within the application:

Injected a simple alert(1) XSS payload

CSRF token validation bypass via HTTP Method Tampering

The application processes input parameters from several routes, utilising the $_REQUEST global variable, therefore accepting both POST and GET HTTP requests.

...
widgetInfoList = Zend_Json::decode($_REQUEST['widgetInfoList']);
...

code/modules/Home/HomeWidgetBlockList.php

Due to this behaviour, combined with insufficient validation of CSRF tokens in include/utils/VteCsrf.php, it is possible to entirely bypass the check for the __csrf_token field.

This can be achieved by switching a POST request to a GET request and omitting the token parameter altogether.

/**
* The name of the magic CSRF token that will be placed in all forms, i.e.
* the contents of <input type="hidden" name="$name" value="CSRF-TOKEN" />
*/
'input-name' => '__csrf_token',
...
public function csrf_check($fatal = true) {
	if ($_SERVER['REQUEST_METHOD'] !== 'POST') return true;
	
	//  csrf_start();
	$name = $this->config['input-name'];
	...
	return $ok;
}

include/utils/VteCsrf.php

As a result, the previously POST-based XSS can be transformed into a traditional GET-based XSS, significantly lowering the exploitation barrier, as it eliminates the need to obtain or predict a valid CSRF token beforehand.

Cross-Site Scripting without the need for __csrf_token

Session cookies in VTENext are secured using the HttpOnly flag, which helps mitigate the risk of arbitrary client-side scripts accessing the cookie.

PHPSESSID cookies protected with the HttpOnly flag

An information disclosure on the Touch module, however, exposes the PHPSESSID value, effectively making the protection of the HttpOnly flag useless.

GET request to index.php?module=Touch&action=ws

In the same way that phpinfo() can be used to leak secured cookies 4 5 6, we can use our oracle to read the victim’s session and steal it through the XSS.

Authentication Bypass: Vector #2

Just like the first one, the second attack vector also relies on user interaction to work.
It uses a similar chain of vulnerabilities to carry out the attack, composed of:

ℹ️
The vulnerability has been patched in version 25.02.2
  • Reflected Cross-Site Scripting (XSS) via POST request (like above)
  • CSRF token validation bypass via HTTP Method Tampering (like above)
  • Authenticated SQL Injection

Authenticated SQL Injection

Multiple Authenticated SQL injections exist in modules/Fax/EditView.php because of how the application builds and executes queries.

...
if($_REQUEST["internal_mailer"] == "true") {
  $smarty->assign('INT_MAILER',"true");
	$rec_type = $_REQUEST["type"];
	$rec_id = $_REQUEST["rec_id"];
	$fieldname = $_REQUEST["fieldname"];
  ...
  if($rec_type == "record_id") {
		$type = $_REQUEST['par_module'];
		//check added for email link in user detail view
		// crmv@64542
		$modInstance = CRMEntity::getInstance($type);
		if(substr($fieldname,0,2)=="cf")
			$tablename = $modInstance->customFieldTable[0];
		else
			$tablename = $modInstance->table_name;
		// crmv@64542e
		if($type == "Users")
			$q = "select $fieldname from $tablename where id=?";	
		elseif($type == "Leads") 
			$q = "select $fieldname from $tablename where leadaddressid=?";
		elseif ($type == "Contacts")
			$q = "select $fieldname from $tablename where contactid=?";
		elseif ($type == "Accounts")
			$q = "select $fieldname from $tablename where accountid=?";
		elseif ($type == "Vendors")
			$q = "select $fieldname from $tablename where vendorid=?";
		$to_fax = $adb->query_result($adb->pquery($q, array($rec_id)),0,$fieldname);
	} elseif ($rec_type == "email_addy") {
		$to_fax = $_REQUEST["email_addy"];
	}
	$smarty->assign('TO_FAX',trim($to_fax,",").",");

modules/Fax/EditView.php

The code in question has two main problems:

  1. Although prepared statements are used with $adb->pquery(), the query that is executed ($q) is created by injecting the user’s input directly inside it, thus making prepared statements useless.
  2. Since we can specify directly via $_REQUEST['fieldname'] the field we want to read, we can basically extract the value of any field within the tables we can access.

Given the points above, a legitimate request has the following format:

GET /index.php?module=Fax&action=EditView&internal_mailer=true
&par_module=Users&fieldname=user_name&type=record_id&rec_id=1 HTTP/1.1
Host: 127.0.0.1:8001
Cookie: [...TRUNCATED...]

The resulting query is:

select user_name from vte_users where id=1;

And the application adds the selected username to the fax recipients:

Admin user added to the Fax receivers

Because we can control via $_REQUEST['fieldname'] which field we want to extract, let’s search for other interesting data. With par_module=Users, we can extract any column from the vte_users table:

vte_users table description

user_password looks to me like a very good candidate, so let’s pass that column to the fieldname HTTP field and extract the current user’s hashed password:

GET /index.php?module=Fax&action=EditView
&internal_mailer=true&par_module=Users&fieldname=user_password
&type=record_id&rec_id=1 HTTP/1.1
Host: trial01.localhost
Cookie: [...TRUNCATED...]

Resulting query:

select user_password from vte_users where id=1;
User admin’s hashed password extracted and added to the Fax receivers

This is already interesting, but extracting the hash of a password then takes time to crack, and furthermore, it does not guarantee 100% success, as the password could be complex and not easily recoverable.

We want an exploit that always works and doesn’t waste our time, right?

Since the query that is constructed directly integrates the input we pass to it, we can inject a subquery that allows us to extract any other field from the DB.

Example of arbitrary extraction of data using a subquery

What other fields in the database could be helpful to us? I think password reset tokens could be just what we need!

vte_userauthtoken table description

In the same way as before, we can build a subquery that extracts the password reset token for a user of our choice, then use this token to set an arbitrary password and log in as that user.

GET /index.php?module=Fax&action=EditView&internal_mailer=true
&par_module=Users&fieldname=(select%20token%20from%20vte_userauthtoken%20where%20userid=1)
&type=record_id&rec_id=1 HTTP/1.1
Host: trial01.localhost
Cookie: [...TRUNCATED...]

The resulting query is:

select (select token from vte_userauthtoken where userid=1) from vte_users where id=1;
Example of arbitrary extraction of a password reset token using a subquery

At this point, we can exfiltrate the token in the same way as we did with the session cookie and complete the attack.

Authentication Bypass: Vector #3

Account takeovers are cool, but the need for user interaction makes it less likely that a user will fall for it. This third attack vector, therefore, will not require any interaction from the user to be completed, just the way we like.

ℹ️
The vulnerability has been patched in version 25.02.1

Arbitrary Password Reset

The hub/rpwd.php password reset endpoint exposes an action (change_password) that does not enforce adequate security validations, making it possible to reset any user’s credentials with only their username.

At [1], the rpwd.php endpoint creates a RecoverPwd() object and calls the process() function, forwarding at the same time, the current request.

require('../config.inc.php');
...
require_once('modules/Users/RecoverPwd.php');

RequestHandler::validateCSRFToken(); // crmv@171581

$RP = new RecoverPwd();
$RP->process($_REQUEST, $_POST); // [1]

hub/rpwd.php

At [2], the action field coming from the forwarded request is read and used at [3] to determine which function to call.

Providing “change_password” as the action, we then enter at [4] the displayChangePwd() function, which also takes an arbitrary user_name value from the original forwarded request and uses it to instantiate at [5] the Users object.

class RecoverPwd {
	public function process(&$request, &$post) {
		global $default_charset;
		$action = $request['action']; // [2]
		
		$smarty = $this->initSmarty();
		header('Content-Type: text/html; charset=' . $default_charset);
		
		if ($action == 'change_password') { // [3]
			$body = $this->displayChangePwd($smarty, $post['user_name'], $post['confirm_new_password']);
		} elseif ($action == 'recover') {
			$body = $this->displayRecoverLandingPage($smarty, $request['key']);
		...
		} elseif ($action == 'change_old_pwd_send') {
			$body = $this->displayChangeOldPwdSend($smarty, $post['key'], $post['old_password'], $post['new_password']);
		} else {
			$body = $this->displayMainForm($smarty);
		}
		...
	}
}
...
public function displayChangePwd($smarty, $username, $newpwd) { // [4]
	// removed validateUserAuthtokenKey, there is already the CSRFT check in rpwd.php
	$current_user = CRMEntity::getInstance('Users');
	$current_user->id = $current_user->retrieve_user_id($username); //[5]
	$current_user->retrieve_entity_info($current_user->id,'Users');
	...
	if (!$current_user->checkPasswordCriteria($newpwd,$current_user->column_fields)) { // [6]
		...
	} elseif ($current_user->id == 1 && isFreeVersion()) { // [7]
		... // for the demo version
	} else {
		$current_user->change_password('oldpwd', $_POST['confirm_new_password'], true, true);
		emptyUserAuthtokenKey($this->user_auth_token_type,$current_user->id); // [8]
		...
	}
}

modules/Users/RecoverPwd.php

If the provided password satisfies the criteria [6] and the instance is not a trial or free version [7], the change_password method of the User object is invoked [8].

Assuming that a user has not enabled 2FA, it is possible to reset a user's password and log in on their behalf via a single HTTP request.


Remote Code Execution

Once we have bypassed the login and obtained authenticated access (ideally with elevated privileges), we can achieve arbitrary code execution in at least two different ways, depending on the conditions we find ourselves in.

Multiple Local File Inclusions

The application contains multiple Local File Inclusion (LFI) vulnerabilities because it incorporates user input into file inclusion functions without proper validation or sanitisation.

In all identified cases, path traversal sequences (e.g., ../) can be used to include arbitrary files, with the only limitation being that the target file must have a .php extension.

Due to the fixes applied to address CVE-2023-46694 and the presence of a comprehensive — though not perfect, for obvious reasons — deny-list of malicious file extensionsno viable method was identified for uploading arbitrary .php files for inclusion. Furthermore, no useful gadgets were found within the pre-existing code base.

// files with one of these extensions will have '.txt' appended to their filename on upload
// upload_badext default value = php, php3, php4, php5, pl, cgi, py, asp, cfm, js, vbs, html, htm
//crmv@16312 crmv@189149 crmv@195993
$upload_badext = array(
	'php', 'php3', 'php4', 'php5', 'pht', 'phtml', 'phps', 'phar',
	'htm', 'html', 'xhtml', 'js', 'pl', 'py', 'rb',
	'cgi', 'asp', 'cfm', 'vbs', 'jsp',
	'exe', 'bin', 'bat', 'com', 'sh', 'dll', 'msi',
	'htaccess', 'htpasswd'
);
//crmv@16312e crmv@189149e crmv@195993e

config.inc.php

RCE using pearcmd.php

Depending on the installation type and the presence of additional software, such as other sites in virtual hosts or extra PHP modules/pluginsexploitable gadgets may be present elsewhere. In such cases, these gadgets can be leveraged to our advantage, thanks to the ability to use path traversal to navigate the file system.

famous example is the presence of the PEAR PHP framework in applications installed by default on many Docker containers that use PHP and most modern-day systems.

If pearcmd.php is present on the system, this well-known technique 8 9 10 can be used to include it, create PHP files with arbitrary content within the web server directory (or anywhere, for later inclusion), ultimately leading to Remote Code Execution (RCE).

Included pearcmd.php and created a gadget file withing the web root containing arbitrary PHP code
Loaded the gadget file and executed the arbitrary code (phpinfo())

Module Upload

VTENext administrators can develop and upload custom modules, user-defined components or extensions that expand the standard functionality of the platform. It is normal that these functions allow arbitrary code to be inserted into the application; thus, this is not a vulnerability.

Management of custom modules within the ModuleManager area

This functionality is accessible via the ModuleMaker and ModuleManager sections, where administrators can create new modules based on standard templates, manage existing ones, export installed modules, or import custom modules developed externally.

VTENext is built on top of Vtiger 5/6 coreand so are its modules. To keep things concise, I won’t delve into the full development process of a custom module here. However, below you’ll find the documentation provided by my assistant (ChatGPT, yes, I’m talking about you!), along with a basic module template to get you started.

  • Vtiger Developer Guide
  • Vtiger Module Developer Guide
  • VtigerCRM_5.2.0_Vtlib.pdf

In short, I created a simple module containing a classic PHP web shell and imported it into the platform via Settings → Module Settings → Custom Modules → Import New Module.

Sample custom module containing the web shell

This effectively gives us code execution, by design.

Conclusion

Despite multiple attempts to responsibly disclose these vulnerabilities, the vendor initially failed to acknowledge or respond for over three months. Meanwhile, following internal VAPTs, a patch was released, with no mention of the issue.

Although vtenext is a relatively unknown application worldwidea fair number of companies in Italy use this solution.

Number of vtenext installations analysed by Censys

Due to its open-source natureother applications derived from vtenext or those with similar origins may also share these same vulnerabilities, potentially increasing the total number of affected sites.

All of this ultimately harms customers, particularly small and medium-sized businesses that often lack robust vulnerability and risk management strategies, leaving them unknowingly exposed to significant risks.

On August 13th, after notification of the article's publication, the vtenext team responded:

"Unfortunately, previous communications sent from a Gmail address may have been marked as spam due to the sender's format (0xbro), and we therefore did not see the message. Some of the vulnerabilities reported have already been corrected recently, as they were detected during VAPT activities conducted by third parties that we commission periodically. We will contact you again as soon as we have the details of the resolution or for any further information, as we always do with independent researchers who write to us, as this is not the first time we have collaborated with freelance professionals. [...] The lack of response was not due to negligence, but to the circumstances described above"

Following a call with the vtenext team on September 4th, it was decided to reduce the technical details of the vulnerabilities to give users time to update to the latest version and the team time to patch the remaining vulnerabilities.

Disclosure Timeline

  • 28/05/2025: Contacted vtenext for the first time through various communication channels, but did not receive any response.
  • 05/06/2025: Contacted vtenext for the second time, but didn’t receive any response again.
  • 09/06/2025: Submitted CVE Request 1879483 to MITRE (still awaiting official CVEs).
  • 13/07/2025: Attempted to contact the developers of vtenext via a direct channel on LinkedIn, but without success.
  • 24/07/2025: Vendor released version 25.02.1 containing a patch for the Arbitrary Password Reset vulnerability.
  • 12/08/2025: Full disclosure, since a patch exists and the grace period has expired.
  • ??/08/2025: Vendor released version 25.02.2 containing a patch for the other vulnerabilities.
  • 04/09/2025: Following a call with the vtenext team, it was decided to reduce the technical details of the vulnerabilities to give users time to update to the latest version and the team time to patch the remaining vulnerabilities.