CVE-2024-9150

2025-02-21
.NET Reflection based Server Side Template Injection in WynEnterprise. Vulnerability write-up from our expert Maksym Brzęczek.

.NET Reflection based Server Side Template Injection in WynEnterprise - CVE-2024-9150

Disclaimer

My knowledge of C#/.NET is not on an expert level, so some provided explanations might be imprecise. There might also be a more efficient way to achieve the same result.

TL;DR

The SSTI vulnerabilities are common and well-researched problems. There are established payloads that can be used to obtain remote code execution without much effort [SSTI (Server Side Template Injection) | HackTricks]. However, a limited amount of information is available about the exploitation of the templates that utilize the C#/.NET. What I think is most interesting in this vulnerability is the use of C# reflection mechanisms to bypass limitations of the executed code which was discovered thanks to the inspiration from an SSTI OffSec OSWE Lab. I hope that the elaboration the process will inspire you dear reader as well.

However, if you are impatient just click here

Let’s start the journey.

Background

The WynEnterprise platform is per product documentation a "business intelligence tool" written in C#/.NET. Its interesting templating functionality can be used to generate custom reports and documents. Functionalities like that are always a good place to take a closer look at. By looking at the documentation [Wyn Enterprise: Expression Editor | Wyn Documentation] we can find a starting point for our research. The "Expression Editor" can be used to define custom code snippets evaluated on the server side.

We can access custom and predefined variables like "&ExecutionTime", "&PageNumber", "&ReportName", as well as logical operators and many built-in functions. At first glance, the function names show some similarities to .NET which would be sensible since the entire application is created in this framework. It is however strange that the variables can start with "&". Let’s keep that in mind. All that executable goodness should be placed in between curly brackets "{…}". This syntax is not consistent with any .NET based templating framework that I know of which suggests a custom solution. Let’s dig deeper!

Pulling on a thread…

Let’s switch gears and try to establish what is happening here. My methodology, always, is to start with basic working functionality and go from there. A report template with a single text field containing a single expression "{Now() + "_benign_code"}" can give us a baseline that shows we can use basic code and is a good starting point for our experiments.
Preview functionality can be used to see the results of code execution.
Our easiest way to achieve remote code execution is probably with the use of "System.Diagnostics.Process" class which simply runs the command provided as an argument of the method "Start". We should also try to retrieve the output of our command. Taking that into account we can arrive at the following payload:
"System.Diagnostics.Process.Start("bash","-c whoami").StandardOutput.ReadToEnd()"
Let’s try that!
We can see that two things have happened. Firstly, the Preview didn’t produce any output.
Also, our Expression was modified in the process of saving.
We should focus on the second problem first. Lack of exact control over the input increases variability and makes it harder to analyze how our actions impact the desired outcome. Fortunately, a quick look at the raw requests shows that the value was modified by the frontend.
Going further, we should modify requests directly to remove this factor. We can also modify the payload so that the resulting render provides us with a little more information about what failed in the process. For example, by making it:
"Whoami result:" + System.Diagnostics.Process.Start("bach","-c whoami").StandardOutput.ReadToEnd()
Now, our output in case of failure tells us if the render failed completely or if we only failed to execute the command and receive the output value.
Taking that into account we arrive at the value "Whoami result: " + System.Diagnostics.Process.Start("bach","-c whoami").StandardOutput.ReadToEnd() which of course needs to be properly escaped.
The rendering process requires a couple of consecutive requests which we will omit here. The last of them shows us the rendered result.

The interesting part

We have failed successfully. Now we know that our code works however, the use of "System.Diagnostics.Process.Start" did not give us any output. There are a couple of things that might have gone wrong. It is worth mentioning that there is no official "eval" equivalent in C# so our code might be compiled before running. It might look like this.
There should be no reason we couldn’t run a code like this.
However, in this case, just because the code compiles does not necessarily mean that the “Process” class is available at runtime.

At this point I tried a couple of different techniques to assess what classes are available but with no success. Using "Type.GetType" method as well as some comparisons based on "typeof" operator failed. Access to the runtime assembly with "System.Reflection.Assembly.GetExecutingAssembly" also didn’t result in satisfactory results. However, inspiration came from a recently solved Server Side Template Injection laboratory from Off Sec Web Expert (OSWE). Let’s take a look at one of the well-known SSTI payloads used in Java Based Templating Engine.

//Using string 'a' to get an instance of class sun.misc.Launcher 

{{'a'.getClass().forName('sun.misc.Launcher').newInstance()}} 

//output: sun.misc.Launcher@715537d4 

It uses an instance of String class to access reflection mechanisms available in the language and set up an instance of a completely different class "sun.misc.Launcher". The code itself is of no use to us but the principle of using reflection mechanisms available in object instances is very interesting…
Some digging in the C#/.NET documentation revealed interesting suspects for further research.
In simple terms, each Object contains a method that can be used to extract the Type and each Type object contains a reference to the current assembly. It should be possible to chain the above method and property to get access to the current runtime and the code should look something like this:
"a".GetType().Assembly
Let’s test this hypothesis!
Our payload resulted in a response value of "System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e". We have access to the assembly object, which opens a lot of interesting possibilities

Enumeration and escalation

Thankfully the documentation [Assembly Class (System.Reflection) | Microsoft Learn] once again is here to help as well. A glance shows us that we can:
Use "Assembly.GetType" and "Assembly.GetTypes" to check the classes and methods that are loaded to the assembly. By supplying payload "a".GetType().Assembly.GetType("System.String") or "a".GetType().Assembly.GetType("System.Diagnostics.Process") we can check if given class is available for us to use.
Our diagnosis is confirmed. The Process class is not available and can’t be used. Now we can use the "Assembly.GetTypes" method to dump the list of all available types. We can search the loaded classes and methods for interesting functionalities like modifying or reading the files, opening sockets, etc. If the conditions are right those can be used to elevate access. However, there may be a better way…

Final nail

The System.Reflection.Assembly class contains static methods that can be used to load DLL’s:
- Assembly.Load
- Assembly.LoadFile
- Assembly.LoadFrom
The DLL that we need to perform an easy remote code execution is already present in the docker container of the application.  We can just load it from the file system. 
Our payload:

"a".GetType().Assembly.GetType("System.Reflection.Assembly").GetMethod("LoadFile").Invoke(null, "/usr/share/dotnet/shared/Microsoft.NETCore.App/6.0.32/System.Diagnostics.Process.dll".Split("?")) 

The result:
(If you are wondering what that "Invoke" is for - this is how the Methods are invoked with the reflection functionalities).
Now we can perform a second level of reflection:

"a".GetType().Assembly.GetType("System.Reflection.Assembly").GetMethod("LoadFile").Invoke(null, "/usr/share/dotnet/shared/Microsoft.NETCore.App/6.0.32/System.Diagnostics.Process.dll".Split("?")).GetType("System.Diagnostics.Process").GetMethods().GetValue(0).Invoke(null, "/bin/bash,-c ""touch /tmp/rce-$(whoami)""".Split(","))

This might look complicated but it is simply a roundabout way of calling  System.Diagnostics.Process.Start("/bin/bash", ",-c ""touch /tmp/rce-$(whoami)"""). After injection, the response indicates that the code runs successfully:
We can also see that the command was executed which proves that we have remote code execution root access.
Do you want to excel in finding vulnerabilities like this one?
Explore OffSec Advanced Web Attacks and Exploitation course!

Summary

When testing this functionality, I didn’t find many resources on using C# reflection and assembly for exploitation so I hope dear reader that you have found this interesting and maybe some information will be helpful to you in the future. An interesting further area for research would be performing the exploitation with the needed DLL, which is unavailable in the filesystem. In theory, it should be possible to inject the bytecode directly from the request by leveraging "Assembly.Load(Byte[])" [Assembly.Load Method (System.Reflection) | Microsoft Learn].
As I mentioned before C#/.NET is not my area of expertise. One of the symptoms of this is that "StandardOutput.ReadToEnd" is dropped in the final payload. This is because in order to use it we would have to start the malicious process with the use of "ProcessStartInfo" class that would have the property "RedirectStandardOutput" set to true. This is doable with the same mechanisms discussed earlier but would make payloads significantly longer and more difficult to understand.

Author

Maksym Brzęczek
IT Security Systems Engineer
Thanks for reading my first write-up! Although I found other vulnerabilities that are on the CVE list this is the first time I decided to share my experience. So, if you have any suggestions or improvements do not hesitate to comment and let me know.

Może zainteresować Cię także:

Cyberprzestępcy uwielbiają tych, którzy... (bezpieczeństwo - 5 najczęstszych błędów) 

Cyberataki zdarzają się każdej minuty, a nieświadomość może nas sporo kosztować. Poznaj najczęstsze błędy, które narażają bezpieczeństwo Twoich danych. Dowiedz się, jak ich unikać i skutecznie zabezpieczyć swoją cyfrową przestrzeń.

Przeczytaj teraz

Leave a Reply

Your email address will not be published. Required fields are marked *

Cybersecurity and data protection.
Penetration, social engineering and performance tests. Security audits and trainings. 
Authorized OffSec partner in Poland.
© 2024 efigo.pl

Stay safe with us.
+48 570 450 695
+48 512 669 907
Efigo Sp. z o.o.
ul. Mikołaja Kopernika 8/6
40-064 Katowice
POLAND

VAT No: PL9542760427
en_GBEN