Research

Path Traversal and Code Execution in CSLA.NET (CVE-2024-28698)

Sam Pizzey
Author
Sam Pizzey
Security Consultant

CSLA.NET is a framework that helps structure business logic for .NET applications into re-usable objects and share those objects between systems. During a penetration test last year, we discovered an interesting path traversal vulnerability affecting applications using this framework. This vulnerability allows an attacker to execute code remotely if they are also able to upload a file to the server, as was the case in our penetration test. This was reported to the vendor, patched in version 8.0, and assigned CVE-2024-28698.

If your application uses CSLA.NET before version 8.0 and allows users to upload files to your web server, you should update the framework as soon as possible. If you are unable to update, you should manually apply this change.

The rest of this post is a technical explanation of how the vulnerability works - and a bonus tip on using null bytes in .NET to truncate paths, like it's 1999. 😎

Discovery

While performing some web application testing for a client, I noticed some responses from /api/DataPortal, which contained an unusual format I wasn't familiar with:

HTTP/1.1 200 OK
Content-Type: application/octet-stream
[…]

:Csla.Server.Hosts.DataPortalChannel.DataPortalResponse, /c […]Csla.Server.Hosts.DataPortalChannel.DataPortalErrorInfo, Csla, Version=7.0.0.0, Culture=neutral, PublicKeyToken=93be5fdc093e4c30ExceptionTypeName-System.IO.FileNotFoundExceptionMessageCould not load file or assembly 'C:\Users\Sam\Downloads\csla-7.0.3 (1)\csla-7.0.3\Samples\BlazorExample\BlazorExample\Server\bin\Debug\net7.0\BlazorExample.Shared\..\..\..\xxx.dll'. The system cannot find the file specified[…]

Note: This format contains non-printable characters between the fields, which are not reproduced here.

It looked similar to BinaryFormatter, which is unsafe to expose to users, but a closer inspection revealed it was a different protocol.

Clues included in the request such as Csla.Server.Hosts.DataPortalChannel pointed to the CSLA.NET project - this endpoint was a CSLA DataPortal which was being exposed to the application’s front-end. The request was using CSLA.NET’s custom MobileFormatter format.

I couldn’t find any previous security research into this format, suggesting that it might be a likely area for finding vulnerabilities. When dealing with unknown protocols, combining fuzzing with Burp Intruder and some guesswork is a good first step for working out potential weak points. Specifically, I often find it worth focusing on fuzzing locations which look like boundaries between fields. When doing this here, the following error was generated:

Request

POST /api/DataPortal?operation=fetch HTTP/1.1
[…]

7Csla.Server.Hosts.DataPortalChannel.CriteriaRequest, /c […]lBlazorExample.Shared.PersonList, BlazorExample.SharedXXX, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null

Response

HTTP/1.1 200 OK
Content-Type: application/octet-stream
[…]

:Csla.Server.Hosts.DataPortalChannel.DataPortalResponse, /c […]Csla.Server.Hosts.DataPortalChannel.DataPortalErrorInfo, Csla, Version=7.0.0.0, Culture=neutral, PublicKeyToken=93be5fdc093e4c30ExceptionTypeName-System.IO.FileNotFoundExceptionMessageCould not load file or assembly 'C:\Users\Sam\Downloads\csla-7.0.3 (1)\csla-7.0.3\Samples\BlazorExample\BlazorExample\Server\bin\Debug\net7.0\BlazorExample.SharedXXX.dll'. The system cannot find the file specified[…]

This error indicated that the application was trying to load a DLL file from a path containing a user-supplied string, and then failing because it doesn't exist. This is unusual and potentially insecure, as if the application loads a user-provided DLL file, an attacker can execute arbitrary code. Next, I tried a path traversal payload in the same location, to see if I could choose the directory the library was loaded from – perhaps one that contains user-uploaded files.

Request

POST /api/DataPortal?operation=fetch HTTP/1.1
[…]

7Csla.Server.Hosts.DataPortalChannel.CriteriaRequest, /c […]lBlazorExample.Shared.PersonList, BlazorExample.Shared\..\..\..\xxx, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null

Response

HTTP/1.1 200 OK
Content-Type: application/octet-stream
[…]

:Csla.Server.Hosts.DataPortalChannel.DataPortalResponse, /c […]Csla.Server.Hosts.DataPortalChannel.DataPortalErrorInfo, Csla, Version=7.0.0.0, Culture=neutral, PublicKeyToken=93be5fdc093e4c30ExceptionTypeName-System.IO.FileNotFoundExceptionMessageCould not load file or assembly 'C:\Users\Sam\Downloads\csla-7.0.3 (1)\csla-7.0.3\Samples\BlazorExample\BlazorExample\Server\bin\Debug\net7.0\BlazorExample.Shared\..\..\..\xxx.dll'. The system cannot find the file specified[…]

This also worked. So, if I was able to upload a DLL file to the server, for example by using file upload functionality provided by the application, I could load it into the web application using this bug and gain code execution, using a library which executes code as soon as it is loaded.

The Details

Taking a look at the code for MobileFormatter, we can see that it can't usually be used with arbitrary objects - only basic .NET types, and classes that implement the IMobileObject interface. But, if we look at the code that performs this check, starting at DeserializeAsDTO, we see that first it calls GetTypeFromCache with the user input as a parameter:

public object DeserializeAsDTO(List<SerializationInfo> deserialized)
{

	_deserializationReferences = new Dictionary<int, IMobileObject>();
	foreach (SerializationInfo info in deserialized)
	{
		var typeName = AssemblyNameTranslator.GetAssemblyQualifiedName(info.TypeName);
		Type type = GetTypeFromCache(typeName);

If this type isn't already cached, the GetTypeFromCache function calls MethodCaller.GetType to get the type name:

private Type GetTypeFromCache(string typeName)
{
	Type result;
	if (!_typeCache.TryGetValue(typeName, out result))
	{
		result = Csla.Reflection.MethodCaller.GetType(typeName);

And this function calls LoadFromAssemblyPath, a .NET standard library function which loads a .NET Assembly from a DLL. This function is used to load the assembly using the name provided by the user, but doesn’t first validate the input is safe against path traversal:

string[] splitName = typeName.Split(',');

if (splitName.Length > 2)
{
	var asm = AssemblyLoadContext.Default.LoadFromAssemblyPath(AppContext.BaseDirectory + splitName[1].Trim() + ".dll");

	return asm.GetType(splitName[0].Trim());

The library should check here that the DLL resides within the application directory to prevent arbitrary DLLs from being loaded from the filesystem - the latest version of CSLA now does this.

Practical Exploitation

Now that we can trick the framework into loading an assembly file of our choice, how can we use this to execute code?

A variety of methods exist for executing code when a DLL is loaded. In this case, I chose to use a native Windows DLL. Even when loading a native library, the .NET AssemblyLoader will still call DllMain, so we can keep our payload simple and avoid having to write a .NET Module Loader:

BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID lpReserved)
{
  switch(reason) {
    case DLL_PROCESS_ATTACH:
      MessageBox(NULL, L"Attacker controlled code displayed me", L"Hello from the DLL", MB_OK);
  }
  
  return TRUE;
}

There is another issue preventing realistic exploitation however - the vulnerable code always appends the extension .dll to the end of the file that it tries to load, meaning that the file needs to be named something.dll. It is common to find a web application that will allow uploading images, text files and so on, but rare to find one that allows us to upload a DLL file.

However, LoadFromAssemblyPath will truncate the assemblyFile parameter when it reaches a null-byte, in a manner very similar to the null-byte Injection attacks in PHP of years gone by. By placing a null-byte after the filename in the serialised data, the '.dll' part will be ignored and we can load our assembly from a  file that the application assumes to be a safe file type, such as an image, as shown in snippet of a malicious request, when viewed in hex format so that the null-byte can be seen:

The above techniques can be combined against the BlazorExample sample project from the CSLA repository. First, the malicious DLL is placed using the filename xxx.jpg into a directory a few levels up from the target application, to simulate file upload functionality as the BlazorExample application doesn’t contain any.

Then, a malicious DataPortal request is made with this filename, using a null-byte at the end to truncate the path. The malicious payload in our library is executed and the dialog box is shown – in this case over the top of the BlazorExample interface since this is a development environment, but in a real application this would be executed on the web server remotely:

As a developer, it’s important to be careful when writing applications that deal with user-supplied file paths – the path must always be validated to ensure nobody can trick the server into using a location you didn’t intend them to. Be careful of differences between operating systems! Some characters that are safe to include in a path on one may be unsafe on another. If unsure, the best bet is always to normalize the path after it has been built (most platforms have a built-in API for doing this), and then after it has been normalized to remove any ‘traversal’ characters, check that the resulting path is where you expect it to be.

Timeline

  • 23 August 2023 - Initial disclosure to the CSLA project.
  • 25 August 2023 - Response from CLSA & a request for more information.
  • 25 August 2023 - Further information provided.
  • 10 November 2023 - Follow-up from Intruder asking if there was any progress on a fix.
  • 10 November 2023 - Potential fix committed by CSLA for review.
  • 13 November 2023 - Fix merged into the CSLA main branch, available to developers.
  • 3 April 2024 - CSLA 8.0 released, which contains the fix.

Sign up for your free 14-day trial

7 days free trial